diff --git a/jena-serviceenhancer/pom.xml b/jena-serviceenhancer/pom.xml index 5bd0da37401..f14a0350453 100644 --- a/jena-serviceenhancer/pom.xml +++ b/jena-serviceenhancer/pom.xml @@ -49,8 +49,8 @@ - junit - junit + org.junit.jupiter + junit-jupiter test diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformAssignToExtend.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformAssignToExtend.java new file mode 100644 index 00000000000..9e236328fb3 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformAssignToExtend.java @@ -0,0 +1,55 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.algebra; + +import org.apache.jena.sparql.algebra.Op; +import org.apache.jena.sparql.algebra.TransformCopy; +import org.apache.jena.sparql.algebra.op.OpAssign; +import org.apache.jena.sparql.algebra.op.OpExtend; + +/** + * Transform OpAssign to OpExtend. + * Rationale: Execution of OpLateral in Jena 5.0.0 inserts OpAssign operations. Attempting to execute those remote results in + * SPARQL LET syntax elements which are usually not understood by remote endpoints. + */ +@Deprecated // Should no longer be necessary with https://github.com/apache/jena/pull/3029 +public class TransformAssignToExtend + extends TransformCopy +{ + private static TransformAssignToExtend INSTANCE = null; + + public static TransformAssignToExtend get() { + if (INSTANCE == null) { + synchronized (TransformAssignToExtend.class) { + if (INSTANCE == null) { + INSTANCE = new TransformAssignToExtend(); + } + } + } + return INSTANCE; + } + + @Override + public Op transform(OpAssign opAssign, Op subOp) { + return OpExtend.create(subOp, opAssign.getVarExprList()); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformSE_EffectiveOptions.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformSE_EffectiveOptions.java index 3f0fc4f5019..8dc6766cc02 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformSE_EffectiveOptions.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformSE_EffectiveOptions.java @@ -25,6 +25,7 @@ import org.apache.jena.sparql.algebra.TransformCopy; import org.apache.jena.sparql.algebra.op.OpService; import org.apache.jena.sparql.service.enhancer.impl.ServiceOpts; +import org.apache.jena.sparql.service.enhancer.impl.ServiceOptsSE; /** * Detects options on SERVICE and materializes them. @@ -49,7 +50,7 @@ public class TransformSE_EffectiveOptions @Override public Op transform(OpService opService, Op subOp) { OpService tmp = new OpService(opService.getService(), subOp, opService.getSilent()); - ServiceOpts so = ServiceOpts.getEffectiveService(tmp); + ServiceOpts so = ServiceOptsSE.getEffectiveService(tmp); OpService result = so.toService(); return result; } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformSE_JoinStrategy.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformSE_JoinStrategy.java index 4319a732507..4b2e2b11271 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformSE_JoinStrategy.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformSE_JoinStrategy.java @@ -18,7 +18,6 @@ * * SPDX-License-Identifier: Apache-2.0 */ - package org.apache.jena.sparql.service.enhancer.algebra; import java.util.HashMap; @@ -45,6 +44,7 @@ import org.apache.jena.sparql.graph.NodeTransform; import org.apache.jena.sparql.graph.NodeTransformLib; import org.apache.jena.sparql.service.enhancer.impl.ServiceOpts; +import org.apache.jena.sparql.service.enhancer.impl.ServiceOptsSE; /** * Checks for the presence of SERVICE <loop:> { } @@ -66,11 +66,22 @@ public Op transform(OpJoin opJoin, Op left, Op right) Op effectiveRight = right; if (right instanceof OpService) { OpService op = (OpService)right; - ServiceOpts opts = ServiceOpts.getEffectiveService(op); - canDoLinear = opts.containsKey(ServiceOpts.SO_LOOP); + ServiceOpts opts = ServiceOptsSE.getEffectiveService(op); + canDoLinear = opts.containsKey(ServiceOptsSE.SO_LOOP); if (canDoLinear) { - NodeTransform joinVarRename = renameForImplicitJoinVars(left); - effectiveRight = NodeTransformLib.transform(joinVarRename, right); + String loopMode = opts.getFirstValue(ServiceOptsSE.SO_LOOP, "", null); + switch (loopMode) { + case "": + NodeTransform joinVarRename = renameForImplicitJoinVars(left); + effectiveRight = NodeTransformLib.transform(joinVarRename, right); + break; + case ServiceOptsSE.SO_LOOP_MODE_SCOPED: + // Nothing to do + effectiveRight = right; + break; + default: + throw new RuntimeException("Unsupported loop mode: " + loopMode); + } } } @@ -92,11 +103,21 @@ public Op transform(OpSequence opSequence, List elts) { Op newOp = right; if (right instanceof OpService) { OpService op = (OpService)right; - ServiceOpts opts = ServiceOpts.getEffectiveService(op); - boolean isLoop = opts.containsKey(ServiceOpts.SO_LOOP); + ServiceOpts opts = ServiceOptsSE.getEffectiveService(op); + boolean isLoop = opts.containsKey(ServiceOptsSE.SO_LOOP); if (isLoop) { - NodeTransform joinVarRename = renameForImplicitJoinVars(visibleVarsLeft); - newOp = NodeTransformLib.transform(joinVarRename, right); + String loopMode = opts.getFirstValue(ServiceOptsSE.SO_LOOP, "", null); + switch (loopMode) { + case "": + NodeTransform joinVarRename = renameForImplicitJoinVars(visibleVarsLeft); + newOp = NodeTransformLib.transform(joinVarRename, right); + break; + case ServiceOptsSE.SO_LOOP_MODE_SCOPED: + // Nothing to do + break; + default: + throw new RuntimeException("Unsupported loop mode: " + loopMode); + } } } @@ -110,33 +131,44 @@ public Op transform(OpSequence opSequence, List elts) { return result; } - @Override - public Op transform(OpDisjunction opSequence, List elts) { - // Accumulated visible vars - Set visibleVarsLeft = new LinkedHashSet<>(); - - OpDisjunction result = OpDisjunction.create(); - for (Op right : elts) { - Op newOp = right; - if (right instanceof OpService) { - OpService op = (OpService)right; - ServiceOpts opts = ServiceOpts.getEffectiveService(op); - boolean isLoop = opts.containsKey(ServiceOpts.SO_LOOP); - if (isLoop) { - NodeTransform joinVarRename = renameForImplicitJoinVars(visibleVarsLeft); - newOp = NodeTransformLib.transform(joinVarRename, right); - } - } - - // Add the now visible vars as new ones - Set visibleVarsRight = OpVars.visibleVars(newOp); - visibleVarsLeft.addAll(visibleVarsRight); - - result.add(newOp); - } - - return result; - } +// @Override +// public Op transform(OpDisjunction opSequence, List elts) { +// // Accumulated visible vars +// Set visibleVarsLeft = new LinkedHashSet<>(); +// +// OpDisjunction result = OpDisjunction.create(); +// for (Op right : elts) { +// Op newOp = right; +// if (right instanceof OpService) { +// OpService op = (OpService)right; +// ServiceOpts opts = ServiceOptsSE.getEffectiveService(op); +// boolean isLoop = opts.containsKey(ServiceOptsSE.SO_LOOP); +// if (isLoop) { +// String loopMode = opts.getFirstValue(ServiceOptsSE.SO_LOOP, "", null); +// switch (loopMode) { +// case "": +// NodeTransform joinVarRename = renameForImplicitJoinVars(visibleVarsLeft); +// newOp = NodeTransformLib.transform(joinVarRename, right); +// break; +// case ServiceOptsSE.SO_LOOP_MODE_SCOPED: +// // Nothing to do +// newOp = right; +// break; +// default: +// throw new RuntimeException("Unsupported loop mode: " + loopMode); +// } +// } +// } +// +// // Add the now visible vars as new ones +// Set visibleVarsRight = OpVars.visibleVars(newOp); +// visibleVarsLeft.addAll(visibleVarsRight); +// +// result.add(newOp); +// } +// +// return result; +// } @Override public Op transform(OpLeftJoin opLeftJoin, Op left, Op right) @@ -145,16 +177,26 @@ public Op transform(OpLeftJoin opLeftJoin, Op left, Op right) Op effectiveRight = right; if (right instanceof OpService) { OpService op = (OpService)right; - ServiceOpts opts = ServiceOpts.getEffectiveService(op); - canDoLinear = opts.containsKey(ServiceOpts.SO_LOOP); + ServiceOpts opts = ServiceOptsSE.getEffectiveService(op); + canDoLinear = opts.containsKey(ServiceOptsSE.SO_LOOP); if (canDoLinear) { - NodeTransform joinVarRename = renameForImplicitJoinVars(left); - effectiveRight = NodeTransformLib.transform(joinVarRename, right); - - ExprList joinExprs = opLeftJoin.getExprs(); - if (joinExprs != null) { - ExprList effectiveExprs = NodeTransformLib.transform(joinVarRename, joinExprs); - effectiveRight = OpFilter.filterBy(effectiveExprs, effectiveRight); + String loopMode = opts.getFirstValue(ServiceOptsSE.SO_LOOP, "", null); + switch (loopMode) { + case "": + NodeTransform joinVarRename = renameForImplicitJoinVars(left); + effectiveRight = NodeTransformLib.transform(joinVarRename, right); + + ExprList joinExprs = opLeftJoin.getExprs(); + if (joinExprs != null) { + ExprList effectiveExprs = NodeTransformLib.transform(joinVarRename, joinExprs); + effectiveRight = OpFilter.filterBy(effectiveExprs, effectiveRight); + } + break; + case ServiceOptsSE.SO_LOOP_MODE_SCOPED: + // Nothing to do + break; + default: + throw new RuntimeException("Unsupported loop mode: " + loopMode); } } } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformSE_OptimizeSelfJoin.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformSE_OptimizeSelfJoin.java index b32cd9b5eed..42d65a180b7 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformSE_OptimizeSelfJoin.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/algebra/TransformSE_OptimizeSelfJoin.java @@ -26,6 +26,7 @@ import org.apache.jena.sparql.algebra.op.OpService; import org.apache.jena.sparql.algebra.optimize.Rewrite; import org.apache.jena.sparql.service.enhancer.impl.ServiceOpts; +import org.apache.jena.sparql.service.enhancer.impl.ServiceOptsSE; import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerConstants; /** It seems that preemtive optimization before execution does not work with property @@ -44,17 +45,17 @@ public TransformSE_OptimizeSelfJoin(Rewrite selfRewrite) { @Override public Op transform(OpService opService, Op subOp) { Op result; - ServiceOpts so = ServiceOpts.getEffectiveService( + ServiceOpts so = ServiceOptsSE.getEffectiveService( new OpService(opService.getService(), subOp, opService.getSilent())); OpService targetService = so.getTargetService(); if (ServiceEnhancerConstants.SELF.equals(targetService.getService())) { - String optimizerOpt = so.getFirstValue(ServiceOpts.SO_OPTIMIZE, "on", "on"); + String optimizerOpt = so.getFirstValue(ServiceOptsSE.SO_OPTIMIZE, "on", "on"); if (!optimizerOpt.equalsIgnoreCase("off")) { Op newSub = selfRewrite.rewrite(targetService.getSubOp()); - so.removeKey(ServiceOpts.SO_OPTIMIZE); + so.removeKey(ServiceOptsSE.SO_OPTIMIZE); // so.add(ServiceOpts.SO_OPTIMIZE, "off"); // so.add(ServiceOpts.SO_OPTIMIZE, "on"); result = new ServiceOpts( diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/assembler/DatasetAssemblerServiceEnhancer.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/assembler/DatasetAssemblerServiceEnhancer.java index 46262d9d4e6..a93741b745b 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/assembler/DatasetAssemblerServiceEnhancer.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/assembler/DatasetAssemblerServiceEnhancer.java @@ -23,12 +23,9 @@ import java.util.Objects; -import com.google.common.base.Preconditions; - import org.apache.commons.lang3.function.TriFunction; import org.apache.jena.assembler.Assembler; import org.apache.jena.assembler.exceptions.AssemblerException; -import org.apache.jena.atlas.logging.Log; import org.apache.jena.graph.Node; import org.apache.jena.query.ARQ; import org.apache.jena.query.Dataset; @@ -39,14 +36,20 @@ import org.apache.jena.sparql.core.DatasetGraph; import org.apache.jena.sparql.core.DatasetGraphWrapper; import org.apache.jena.sparql.core.assembler.DatasetAssembler; +import org.apache.jena.sparql.core.assembler.DatasetAssemblerVocab; import org.apache.jena.sparql.service.enhancer.impl.ChainingServiceExecutorBulkCache; import org.apache.jena.sparql.service.enhancer.impl.ServiceResponseCache; import org.apache.jena.sparql.service.enhancer.impl.util.GraphUtilsExtra; +import org.apache.jena.sparql.service.enhancer.impl.util.Lazy; import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerConstants; import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerInit; import org.apache.jena.sparql.util.Context; import org.apache.jena.sparql.util.Symbol; import org.apache.jena.sparql.util.graph.GraphUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Preconditions; /** * Assembler that sets up a base dataset's context with the service enhancer machinery. @@ -55,9 +58,26 @@ public class DatasetAssemblerServiceEnhancer extends DatasetAssembler { + private static final Logger logger = LoggerFactory.getLogger(DatasetAssemblerServiceEnhancer.class); + + @SuppressWarnings("deprecation") @Override public DatasetGraph createDataset(Assembler a, Resource root) { Resource baseDatasetRes = GraphUtils.getResourceValue(root, ServiceEnhancerVocab.baseDataset); + if (baseDatasetRes != null) { + if (logger.isWarnEnabled()) { + logger.warn("Use of {} is deprecated. Please use {} instead.", ServiceEnhancerVocab.baseDataset.getURI(), DatasetAssemblerVocab.pDataset.getURI() ); + } + } + + Resource tmp = GraphUtils.getResourceValue(root, DatasetAssemblerVocab.pDataset); + if (tmp != null) { + if (baseDatasetRes != null) { + throw new AssemblerException(root, "Both " + DatasetAssemblerVocab.pDataset.getURI() + " and " + ServiceEnhancerVocab.baseDataset.getURI() + " specified. Please only use the former."); + } + baseDatasetRes = tmp; + } + Objects.requireNonNull(baseDatasetRes, "No ja:baseDataset specified on " + root); Object obj = a.open(baseDatasetRes); @@ -92,7 +112,7 @@ public DatasetGraph createDataset(Assembler a, Resource root) { Preconditions.checkArgument(maxPageCount > 0, ServiceEnhancerVocab.cacheMaxPageCount.getURI() + " requires a value greater than 0"); ServiceResponseCache cache = new ServiceResponseCache(maxEntryCount, pageSize, maxPageCount); - ServiceResponseCache.set(cxt, cache); + ServiceResponseCache.set(cxt, Lazy.ofInstance(cache)); } // Transfer values from the RDF model to the context @@ -113,7 +133,9 @@ public DatasetGraph createDataset(Assembler a, Resource root) { result = DatasetFactory.wrap(new DatasetGraphWrapper(result.asDatasetGraph(), cxt)); } - Log.info(DatasetAssemblerServiceEnhancer.class, "Dataset self id set to " + selfId); + if (logger.isInfoEnabled()) { + logger.info("Dataset self id set to {}", selfId); + } } else { Class cls = obj == null ? null : obj.getClass(); throw new AssemblerException(root, "Expected ja:baseDataset to be a Dataset but instead got " + Objects.toString(cls)); @@ -122,7 +144,6 @@ public DatasetGraph createDataset(Assembler a, Resource root) { return result.asDatasetGraph(); } - /** Transfer a resource's property value to a context symbol's value */ private static void configureCxt(Resource root, Property property, Context cxt, Symbol symbol, boolean applyDefaultValueIfPropertyAbsent, T defaultValue, TriFunction getValue) { if (root.hasProperty(property) || applyDefaultValueIfPropertyAbsent) { diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/assembler/ServiceEnhancerVocab.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/assembler/ServiceEnhancerVocab.java index f1e393b6bb6..f50eeca4fdf 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/assembler/ServiceEnhancerVocab.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/assembler/ServiceEnhancerVocab.java @@ -27,6 +27,7 @@ import org.apache.jena.rdf.model.ResourceFactory; import org.apache.jena.riot.system.PrefixMap; import org.apache.jena.shared.PrefixMapping; +import org.apache.jena.sparql.core.assembler.DatasetAssemblerVocab; /** Vocabulary for assembler-based configuration of the service enhancer plugin */ public class ServiceEnhancerVocab { @@ -42,8 +43,10 @@ public class ServiceEnhancerVocab { /** Enable privileged management functions; creates a wrapped dataset with a copied context */ public static final Property enableMgmt = ResourceFactory.createProperty(NS + "enableMgmt"); - /** The term "baseDataset" is not officially in ja but it seems reasonable to eventually add it there. - * So far ja only defines baseModel */ + /** + * Deprecated in favor of {@link DatasetAssemblerVocab#pDataset}. + */ + @Deprecated public static final Property baseDataset = ResourceFactory.createProperty(JA.getURI() + "baseDataset"); /** Maximum number of entries the service cache can hold */ @@ -70,7 +73,7 @@ public class ServiceEnhancerVocab { * se{@value #NS} * */ - public PrefixMap addPrefixes(PrefixMap pm) { + public static PrefixMap addPrefixes(PrefixMap pm) { pm.add("ja", JA.getURI()); pm.add("se", ServiceEnhancerVocab.getURI()); return pm; @@ -83,7 +86,7 @@ public PrefixMap addPrefixes(PrefixMap pm) { * se{@value #NS} * */ - public PrefixMapping addPrefixes(PrefixMapping pm) { + public static PrefixMapping addPrefixes(PrefixMapping pm) { pm.setNsPrefix("ja", JA.getURI()); pm.setNsPrefix("se", ServiceEnhancerVocab.getURI()); return pm; diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/AsyncClaimingCache.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/AsyncClaimingCache.java index 072b16c0540..bc9dce5e4da 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/AsyncClaimingCache.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/AsyncClaimingCache.java @@ -24,7 +24,7 @@ import java.util.Collection; import java.util.function.Predicate; -import org.apache.jena.sparql.service.enhancer.slice.api.Disposable; +import org.apache.jena.atlas.lib.Closeable; /** * Interface for an async cache that allows "claiming" entries. @@ -52,11 +52,24 @@ public interface AsyncClaimingCache { /** * Protect eviction of certain keys as long as the guard is not disposed. * Disposable may immediately evict all no longer guarded items */ - Disposable addEvictionGuard(Predicate predicate); + Closeable addEvictionGuard(Predicate predicate); /** Return a snapshot of all present keys */ Collection getPresentKeys(); + /** + * Synchronous(!) invalidation of all keys. + * The cache is expected to be empty when this method returns in a non-concurrent setting. + */ void invalidateAll(); + + /** + * Synchronous(!) invalidation of the given keys. + * The cache entries for the given keys are expected to be removed when this method returns + * in a non-concurrent setting. + */ void invalidateAll(Iterable keys); + + /** Run maintenance actions as needed, such as eviction of keys */ + void cleanUp(); } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/AsyncClaimingCacheImplCaffeine.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/AsyncClaimingCacheImplCaffeine.java new file mode 100644 index 00000000000..a36726010d9 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/AsyncClaimingCacheImplCaffeine.java @@ -0,0 +1,460 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.claimingcache; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.BiConsumer; +import java.util.function.Function; +import java.util.function.Predicate; + +import org.apache.jena.atlas.lib.Closeable; +import org.apache.jena.sparql.service.enhancer.concurrent.AutoLock; +import org.apache.jena.sparql.service.enhancer.util.LinkedList; +import org.apache.jena.sparql.service.enhancer.util.LinkedList.LinkedListNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.AsyncCache; +import com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.RemovalCause; +import com.github.benmanes.caffeine.cache.RemovalListener; +import com.google.common.collect.Sets; + +/** + * Implementation of async claiming cache. + * Claimed entries will never be evicted. Conversely, unclaimed items are added to a cache such that timely re-claiming + * will be fast. + * + * Use cases: + * - Resource sharing: Ensure that the same resource is handed to all clients requesting one by key. + * - Resource pooling: Claimed resources will never be closed, but unclaimed resources (e.g. something backed by an input stream) + * may remain on standby for a while. + * + * Another way to view this class is as a mix of a map with weak values and a cache. + * + * @param + * @param + */ +public class AsyncClaimingCacheImplCaffeine + implements AsyncClaimingCache +{ + private static final Logger logger = LoggerFactory.getLogger(AsyncClaimingCacheImplCaffeine.class); + + // level1: Claimed items - those items will never be evicted as long as the references are not closed. + protected Map> level1; + + // level2: The caffine cache - items in this cache are not claimed and are subject to eviction according to configuration. + protected AsyncCache level2; + protected Function> level3AwareCacheLoader; + + // level3: Items evicted from level2 but caught by at least one eviction guard. + protected Map level3; + + // Runs atomically in the claim action after the entry exists in level1. + protected BiConsumer> claimListener; + + // Runs atomically in the unclaim action before the entry is removed from level1. + protected BiConsumer> unclaimListener; + + // A lock that prevents invalidation while entries are being loaded. + protected ReentrantReadWriteLock invalidationLock = new ReentrantReadWriteLock(); + + protected ReentrantReadWriteLock evictionGuardLock; + + // A list of predicates that decide whether a key is considered protected from eviction + // Each predicate abstracts matching a set of keys, e.g. a range of integer values. + // The predicates are assumed to always return the same result for the same argument. + protected final LinkedList> evictionGuards; + + protected RemovalListener atomicRemovalListener; + + protected Set suppressedRemovalEvents; + + public AsyncClaimingCacheImplCaffeine( + Map> level1, + AsyncCache level2, + Function level3AwareCacheLoader, + Map level3, + LinkedList> evictionGuards, + ReentrantReadWriteLock evictionGuardLock, + BiConsumer> claimListener, + BiConsumer> unclaimListener, + RemovalListener atomicRemovalListener, + Set suppressedRemovalEvents + ) { + super(); + this.level1 = level1; + this.level2 = level2; + this.level3AwareCacheLoader = k -> CompletableFuture.completedFuture(level3AwareCacheLoader.apply(k)); + this.level3 = level3; + this.evictionGuards = evictionGuards; + this.evictionGuardLock = evictionGuardLock; + this.claimListener = claimListener; + this.unclaimListener = unclaimListener; + this.atomicRemovalListener = atomicRemovalListener; + this.suppressedRemovalEvents = suppressedRemovalEvents; + } + + @Override + public void cleanUp() { + level2.synchronous().cleanUp(); + } + + protected SynchronizerMap synchronizerMap = new SynchronizerMap<>(); + + /** + * Registers a predicate that 'caches' entries about to be evicted + * When closing the registration then keys that have not moved back into the cache + * by reference will be immediately evicted. + */ + @Override + public Closeable addEvictionGuard(Predicate predicate) { + LinkedListNode> linkedListNode; + try (AutoLock lock = AutoLock.lock(evictionGuardLock.writeLock())) { + if (logger.isDebugEnabled()) { + logger.debug("Registering eviction guard: {}. Already active guards: {}.", predicate, evictionGuards); + } + linkedListNode = evictionGuards.append(predicate); + } + return () -> { + try (AutoLock lock = AutoLock.lock(evictionGuardLock.writeLock())) { + if (!linkedListNode.isLinked()) { + throw new IllegalStateException("Eviction guard " + predicate + " has already been removed."); + } + + linkedListNode.unlink(); + if (logger.isDebugEnabled()) { + logger.debug("Removed eviction guard: {}. Now active guards: {}.", predicate, evictionGuards); + } + runLevel3Eviction(); + } + }; + } + + /** + * Remove all items from level3 that do not match any eviction guard. + * Called while being synchronized on the evictionGuards. + */ + protected void runLevel3Eviction() { + Iterator> it = level3.entrySet().iterator(); + while (it.hasNext()) { + Entry e = it.next(); + K k = e.getKey(); + V v = e.getValue(); + + boolean isGuarded = evictionGuards.stream().anyMatch(p -> p.test(k)); + if (!isGuarded) { + if (logger.isDebugEnabled()) { + logger.debug("Evicting key {} which is no longer protected by remaining guards {}.", k, evictionGuards); + } + + atomicRemovalListener.onRemoval(k, v, RemovalCause.COLLECTED); + + // Remove the key from level 3. + // This is the only place where entries are removed from level 3 + it.remove(); + } + } + } + + @Override + public RefFuture claim(K key) { + RefFuture result = synchronizerMap.compute(key, synchronizer -> { + Holder isFreshSecondaryRef = Holder.of(Boolean.FALSE); + + // Guard against concurrent invalidation requests + RefFuture secondaryRef; + + try (AutoLock autoLock = AutoLock.lock(invalidationLock.readLock())) { + secondaryRef = level1.computeIfAbsent(key, k -> { + // Wrap the loaded reference such that closing the fully loaded reference adds it to level 2 + + if (logger.isTraceEnabled()) { + logger.trace("Claiming item [{}] from level2.", key); + } + CompletableFuture future = level2.get(key, (kk, executor) -> level3AwareCacheLoader.apply(kk)); + + // level2.invalidate(key) triggers level2's removal listener but we are about to add the item to level1 + // so we don't want to publish a removal event to the outside + suppressedRemovalEvents.add(key); + level2.synchronous().invalidate(key); + suppressedRemovalEvents.remove(key); + + Holder> holder = Holder.of(null); + Ref> freshSecondaryRef = + RefImpl.create(future, synchronizer, () -> { + + // This is the unclaim action. + + RefFuture v = holder.get(); + + if (unclaimListener != null) { + unclaimListener.accept(key, v); + } + + // If the future has not completed yet then cancel it (nothing is done for completed futures) + // Then the future is added to level2. + // If the future fails then the cache entry is removed according to the cache API contract; + // otherwise the value will be readily available. + RefFutureImpl.cancelFutureOrCloseValue(future, null); + level1.remove(key); + if (logger.isTraceEnabled()) { + logger.trace("Item [{}] was unclaimed. Transferring to level2.", key); + } + level2.put(key, future); + + // Run a check whether to free the key's proxy object + // in the synchronizerMap if the count is zero + synchronizer.clearEntryIfZero(); + }); + isFreshSecondaryRef.set(Boolean.TRUE); + RefFuture r = RefFutureImpl.wrap(freshSecondaryRef); + holder.set(r); + return r; + }); + } + + RefFuture r = secondaryRef.acquire("secondary ref"); + if (claimListener != null) { + claimListener.accept(key, r); + } + + if (isFreshSecondaryRef.get()) { + secondaryRef.close(); + } + return r; + }); + + return result; + } + + public static class Builder + { + protected Caffeine caffeine; + protected CacheLoader cacheLoader; + protected BiConsumer> claimListener; + protected BiConsumer> unclaimListener; + protected RemovalListener atomicRemovalListener; + + Builder setCaffeine(Caffeine caffeine) { + this.caffeine = caffeine; + return this; + } + + public Builder setClaimListener(BiConsumer> claimListener) { + this.claimListener = claimListener; + return this; + } + + public Builder setUnclaimListener(BiConsumer> unclaimListener) { + this.unclaimListener = unclaimListener; + return this; + } + + public Builder setCacheLoader(CacheLoader cacheLoader) { + this.cacheLoader = cacheLoader; + return this; + } + + /** + * The given removal listener is run atomically both during eviction and invalidation. + */ + public Builder setAtomicRemovalListener(RemovalListener atomicRemovalListener) { + this.atomicRemovalListener = atomicRemovalListener; + return this; + } + + @SuppressWarnings("unchecked") + public AsyncClaimingCacheImplCaffeine build() { + Map> level1 = new ConcurrentHashMap<>(); + Map level3 = new ConcurrentHashMap<>(); + LinkedList> evictionGuards = new LinkedList<>(); + ReentrantReadWriteLock evictionGuardLock = new ReentrantReadWriteLock(); + + RemovalListener level3AwareAtomicRemovalListener = (k, v, c) -> { + if (logger.isDebugEnabled()) { + logger.debug("Removal triggered. Key: {}, Value: {}, Cause: {}.", k, v, c); + } + // Check for actual removal - key no longer present in level1. + if (!level1.containsKey(k)) { + + boolean isGuarded = false; + try (AutoLock lock = AutoLock.lock(evictionGuardLock.writeLock())) { + // Check whether this eviction from level2 should be caught by an eviction guard. + for (Predicate evictionGuard : evictionGuards) { + isGuarded = evictionGuard.test(k); + if (isGuarded) { + if (logger.isDebugEnabled()) { + logger.debug("Protecting this key from eviction: {}. Number of already protected keys: {}.", k, level3.size()); + } + + // try (AutoLock writeLock = AutoLock.lock(evictionGuardLock.writeLock())) { + // FIXME level3 is not a concurrent map - either change the map type or synchronize! + level3.put(k, v); + // } + break; + } + } + + if (!isGuarded) { + if (atomicRemovalListener != null) { + atomicRemovalListener.onRemoval(k, v, c); + } + } + } + } + }; + + Set suppressedRemovalEvents = Sets.newConcurrentHashSet(); + + // Eviction listener is part of the cache's atomic operation + // Important: The caffeine.evictionListener is atomic but NEVER called + // as a consequence of cache.invalidateAll() + Caffeine finalLevel2Builder = caffeine.evictionListener((k, v, c) -> { + K kk = (K)k; + boolean isEventSuppressed = suppressedRemovalEvents.contains(kk); + if (!isEventSuppressed) { + // CompletableFuture cfv = (CompletableFuture)v; + CompletableFuture cfv = CompletableFuture.completedFuture((V)v); + V vv = null; + if (cfv.isDone()) { + try { + vv = cfv.get(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException("Should not happen", e); + } + } + level3AwareAtomicRemovalListener.onRemoval(kk, vv, c); + } + }); + + // Cache loader that checks for existing items in level + Function level3AwareCacheLoader = k -> { + Object[] tmp = new Object[] { null }; + // Atomically get and remove an existing key from level3. + // Protect the access from concurrent eviction. + try (AutoLock lock = AutoLock.lock(evictionGuardLock.writeLock())) { + level3.compute(k, (kk, v) -> { + tmp[0] = v; + return null; + }); + } + + V r = (V)tmp[0]; + if (r == null) { + try { + r = cacheLoader.load(k); + } catch (Exception e) { + e.printStackTrace(); + throw new RuntimeException(e); + } + } + return r; + }; + + AsyncCache level2 = finalLevel2Builder.buildAsync(); + + return new AsyncClaimingCacheImplCaffeine<>(level1, level2, level3AwareCacheLoader, level3, evictionGuards, evictionGuardLock, claimListener, unclaimListener, level3AwareAtomicRemovalListener, suppressedRemovalEvents); + } + } + + public static Builder newBuilder(Caffeine caffeine) { + Builder result = new Builder<>(); + result.setCaffeine(caffeine); + return result; + } + + /** + * Claim a key only if it is already present. + * + * This implementation is a best effort approach: + * There is a very slim chance that just between testing a key for presence and claiming its entry + * an eviction occurs - causing claiming of a non-present key and thus triggering a load action. + */ + @Override + public RefFuture claimIfPresent(K key) { + RefFuture result = level1.containsKey(key) || level2.asMap().containsKey(key) ? claim(key) : null; + return result; + } + + @Override + public void invalidateAll() { + try (AutoLock autoLock = AutoLock.lock(invalidationLock.writeLock())) { + // Cache synchronousCache = level2.synchronous(); + //synchronousCache.asMap().keySet()); + List keys = new ArrayList<>(level2.asMap().keySet()); + invalidateAllInternal(keys); + } + } + + @Override + public void invalidateAll(Iterable keys) { + try (AutoLock autoLock = AutoLock.lock(invalidationLock.writeLock())) { + invalidateAllInternal(keys); + } + } + + private void invalidateAllInternal(Iterable keys) { + Map> map = level2.asMap(); + for (K key : keys) { + map.compute(key, (k, vFuture) -> { + V v = null; + if (vFuture != null && vFuture.isDone()) { + try { + v = vFuture.get(); + } catch (Exception e) { + if (logger.isWarnEnabled()) { + logger.warn("Detected cache entry that failed to load during invalidation", e); + } + } + atomicRemovalListener.onRemoval(k, v, RemovalCause.EXPLICIT); + } + return null; + }); + } + } + + /** + * This method returns a snapshot of all keys across all internal cache levels. + * It should only be used for informational purposes. + */ + @Override + public Collection getPresentKeys() { + Set result = new LinkedHashSet<>(); + result.addAll(level1.keySet()); + result.addAll(level2.asMap().keySet()); + result.addAll(level3.keySet()); + return result; + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/AsyncClaimingCacheImplGuava.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/AsyncClaimingCacheImplGuava.java deleted file mode 100644 index f68f78fd09a..00000000000 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/AsyncClaimingCacheImplGuava.java +++ /dev/null @@ -1,441 +0,0 @@ -/* - * 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 - * - * https://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. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.sparql.service.enhancer.claimingcache; - -import java.util.*; -import java.util.Map.Entry; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.ReentrantReadWriteLock; -import java.util.function.BiConsumer; -import java.util.function.Function; -import java.util.function.Predicate; - -import com.google.common.cache.*; - -import org.apache.jena.sparql.service.enhancer.impl.util.LockUtils; -import org.apache.jena.sparql.service.enhancer.slice.api.Disposable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Implementation of async claiming cache. - * Claimed entries will never be evicted. Conversely, unclaimed items remain are added to a cache such that timely re-claiming - * will be fast. - * - * Use cases: - *
    - *
  • Resource sharing: Ensure that the same resource is handed to all clients requesting one by key.
  • - *
  • Resource pooling: Claimed resources will never be closed, but unclaimed resources (e.g. something backed by an input stream) - * may remain on standby for a while.
  • - *
- * - * Another way to view this class is as a mix of a map with weak values and a cache. - * - * @param The key type - * @param The value type - */ -public class AsyncClaimingCacheImplGuava - implements AsyncClaimingCache -{ - private static final Logger logger = LoggerFactory.getLogger(AsyncClaimingCacheImplGuava.class); - - // level1: claimed items - those items will never be evicted as long as the references are not closed - protected Map> level1; - - // level2: the caffine cache - items in this cache are not claimed are subject to eviction according to configuration - protected LoadingCache> level2; - - // level3: items evicted from level2 but caught be eviction protection - protected Map level3; - - // Runs atomically in the claim action after the entry exists in level1 - protected BiConsumer> claimListener; - - // Runs atomically in the unclaim action before the entry is removed from level1 - protected BiConsumer> unclaimListener; - - // A lock that prevents invalidation while entries are being loaded - protected ReentrantReadWriteLock invalidationLock = new ReentrantReadWriteLock(); - - // A collection of deterministic predicates for 'catching' entries evicted by level2 - // Caught entries are added to level3 - protected final Collection> evictionGuards; - - // Runs atomically when an item is evicted or invalidated and will thus no longer be present in any levels - // See also https://github.com/ben-manes/caffeine/wiki/Removal - protected RemovalListener atomicRemovalListener; - - protected Set suppressedRemovalEvents; - - public AsyncClaimingCacheImplGuava( - Map> level1, - LoadingCache> level2, - Map level3, - Collection> evictionGuards, - BiConsumer> claimListener, - BiConsumer> unclaimListener, - RemovalListener atomicRemovalListener, - Set suppressedRemovalEvents - ) { - super(); - this.level1 = level1; - this.level2 = level2; - this.level3 = level3; - this.evictionGuards = evictionGuards; - this.claimListener = claimListener; - this.unclaimListener = unclaimListener; - this.atomicRemovalListener = atomicRemovalListener; - this.suppressedRemovalEvents = suppressedRemovalEvents; - } - - protected Map keyToSynchronizer = new ConcurrentHashMap<>(); - - /** - * Registers a predicate that 'caches' entries about to be evicted - * When closing the registration then keys that have not moved back into the ache - * by reference will be immediately evicted. - */ - @Override - public Disposable addEvictionGuard(Predicate predicate) { - // Note: LinkedList.listIterator() becomes invalidated after any modification - // In principle a LinkedList would be the more appropriate data structure - synchronized (evictionGuards) { - evictionGuards.add(predicate); - } - - return () -> { - synchronized (evictionGuards) { - evictionGuards.remove(predicate); - runLevel3Eviction(); - } - }; - } - - /** Called while being synchronized on the evictionGuards */ - protected void runLevel3Eviction() { - Iterator> it = level3.entrySet().iterator(); - while (it.hasNext()) { - Entry e = it.next(); - K k = e.getKey(); - V v = e.getValue(); - - boolean isGuarded = evictionGuards.stream().anyMatch(p -> p.test(k)); - if (!isGuarded) { - atomicRemovalListener.onRemoval(RemovalNotification.create(k, v, RemovalCause.COLLECTED)); - it.remove(); - } - } - } - - @Override - public RefFuture claim(K key) { - RefFuture result; - - // We rely on ConcurrentHashMap.compute operating atomically - Latch synchronizer = keyToSynchronizer.compute(key, (k, before) -> before == null ? new Latch() : before.inc()); - - // /guarded_entry/ marker; referenced in comment below - - synchronized (synchronizer) { - keyToSynchronizer.compute(key, (k, before) -> before.dec()); - boolean[] isFreshSecondaryRef = { false }; - - // Guard against concurrent invalidations - RefFuture secondaryRef = LockUtils.runWithLock(invalidationLock.readLock(), () -> { - return level1.computeIfAbsent(key, k -> { - // Wrap the loaded reference such that closing the fully loaded reference adds it to level 2 - - logger.trace("Claiming item [" + key + "] from level2"); - CompletableFuture future; - try { - future = level2.get(key); - } catch (ExecutionException e) { - throw new RuntimeException("Should not happen", e); - } - - // This triggers removal - suppressedRemovalEvents.add(key); - level2.asMap().remove(key); - suppressedRemovalEvents.remove(key); - - @SuppressWarnings("unchecked") - RefFuture[] holder = new RefFuture[] {null}; - - Ref> freshSecondaryRef = - RefImpl.create(future, synchronizer, () -> { - - // This is the unclaim action - - RefFuture v = holder[0]; - - if (unclaimListener != null) { - unclaimListener.accept(key, v); - } - - RefFutureImpl.cancelFutureOrCloseValue(future, null); - level1.remove(key); - logger.trace("Item [" + key + "] was unclaimed. Transferring to level2."); - level2.put(key, future); - - // If there are no waiting threads we can remove the latch - keyToSynchronizer.compute(key, (kk, before) -> before.get() == 0 ? null : before); - // syncRef.close(); - }); - isFreshSecondaryRef[0] = true; - - RefFuture r = RefFutureImpl.wrap(freshSecondaryRef); - holder[0] = r; - - return r; - }); - }); - - result = secondaryRef.acquire(); - - if (claimListener != null) { - claimListener.accept(key, result); - } - - if (isFreshSecondaryRef[0]) { - secondaryRef.close(); - } - } - - return result; - } - - public static class Builder - { - protected CacheBuilder cacheBuilder; - protected Function cacheLoader; - protected BiConsumer> claimListener; - protected BiConsumer> unclaimListener; - protected RemovalListener userAtomicRemovalListener; - - Builder setCacheBuilder(CacheBuilder caffeine) { - this.cacheBuilder = caffeine; - return this; - } - - public Builder setClaimListener(BiConsumer> claimListener) { - this.claimListener = claimListener; - return this; - } - - public Builder setUnclaimListener(BiConsumer> unclaimListener) { - this.unclaimListener = unclaimListener; - return this; - } - - public Builder setCacheLoader(Function cacheLoader) { - this.cacheLoader = cacheLoader; - return this; - } - - public Builder setAtomicRemovalListener(RemovalListener userAtomicRemovalListener) { - this.userAtomicRemovalListener = userAtomicRemovalListener; - return this; - } - - @SuppressWarnings("unchecked") - public AsyncClaimingCacheImplGuava build() { - - Map> level1 = new ConcurrentHashMap<>(); - Map level3 = new ConcurrentHashMap<>(); - Collection> evictionGuards = new ArrayList<>(); - - RemovalListener level3AwareAtomicRemovalListener = n -> { - K k = n.getKey(); - V v = n.getValue(); - RemovalCause c = n.getCause(); - - // Check for actual removal - key no longer present in level1 - if (!level1.containsKey(k)) { - - boolean isGuarded = false; - synchronized (evictionGuards) { - // Check for an eviction guard - for (Predicate evictionGuard : evictionGuards) { - isGuarded = evictionGuard.test(k); - if (isGuarded) { - logger.debug("Protecting from eviction: " + k + " - " + level3.size() + " items protected"); - level3.put(k, v); - break; - } - } - } - - if (!isGuarded) { - if (userAtomicRemovalListener != null) { - userAtomicRemovalListener.onRemoval(RemovalNotification.create(k, v, c)); - } - } - } - }; - - Set suppressedRemovalEvents = Collections.newSetFromMap(new ConcurrentHashMap()); - - cacheBuilder.removalListener(n -> { - K kk = (K)n.getKey(); - - if (!suppressedRemovalEvents.contains(kk)) { - CompletableFuture cfv = (CompletableFuture)n.getValue(); - - V vv = null; - if (cfv.isDone()) { - try { - vv = cfv.get(); - } catch (InterruptedException | ExecutionException e) { - throw new RuntimeException("Should not happen", e); - } - } - - RemovalCause c = n.getCause(); - - level3AwareAtomicRemovalListener.onRemoval(RemovalNotification.create(kk, vv, c)); - } - }); - - - // Cache loader that checks for existing items in level3 - Function level3AwareCacheLoader = k -> { - Object[] tmp = new Object[] { null }; - // Atomically get and remove an existing key from level3 - level3.compute(k, (kk, v) -> { - tmp[0] = v; - return null; - }); - - V r = (V)tmp[0]; - if (r == null) { - r = cacheLoader.apply(k); - } - return r; - }; - - LoadingCache> level2 = cacheBuilder.build( - CacheLoader.from(k -> CompletableFuture.completedFuture(level3AwareCacheLoader.apply(k)))); - - AsyncClaimingCacheImplGuava result = new AsyncClaimingCacheImplGuava<>(level1, level2, level3, evictionGuards, claimListener, unclaimListener, level3AwareAtomicRemovalListener, suppressedRemovalEvents); - return result; - } - } - - public static Builder newBuilder(CacheBuilder caffeine) { - Builder result = new Builder<>(); - result.setCacheBuilder(caffeine); - return result; - } - - public static void main(String[] args) throws InterruptedException { - // TODO This should become a test case that tests the eviction guard feature - - AsyncClaimingCacheImplGuava cache = AsyncClaimingCacheImplGuava.newBuilder( - CacheBuilder.newBuilder().maximumSize(10).expireAfterWrite(1, TimeUnit.SECONDS)) - .setCacheLoader(key -> "Loaded " + key) - .setAtomicRemovalListener(n -> System.out.println("Evicted " + n.getKey())) - .setClaimListener((k, v) -> System.out.println("Claimed: " + k)) - .setUnclaimListener((k, v) -> System.out.println("Unclaimed: " + k)) - .build(); - - try (RefFuture ref = cache.claim("test")) { - try (Disposable disposable = cache.addEvictionGuard(k -> k.contains("test"))) { - System.out.println(ref.await()); - ref.close(); - TimeUnit.SECONDS.sleep(5); - - try (RefFuture reclaim = cache.claim("test")) { - disposable.close(); - // reclaim.close(); - } - } - } - - TimeUnit.SECONDS.sleep(5); - System.out.println("done"); - } - - /** - * Claim a key only if it is already present. - * - * This implementation is a best effort approach: - * There is a very slim chance that just between testing a key for presence and claiming its entry - * an eviction occurs - causing claiming of a non-present key and thus triggering a load action. - */ - @Override - public RefFuture claimIfPresent(K key) { - RefFuture result = level1.containsKey(key) || level2.asMap().containsKey(key) ? claim(key) : null; - return result; - } - - @Override - public void invalidateAll() { - List keys = new ArrayList<>(level2.asMap().keySet()); - invalidateAll(keys); - } - - @Override - public void invalidateAll(Iterable keys) { - LockUtils.runWithLock(invalidationLock.writeLock(), () -> { - Map> map = level2.asMap(); - for (K key : keys) { - map.compute(key, (k, vFuture) -> { - V v = null; - if (vFuture.isDone()) { - try { - v = vFuture.get(); - } catch (Exception e) { - logger.warn("Detected cache entry that failed to load during invalidation", e); - } - } - - atomicRemovalListener.onRemoval(RemovalNotification.create(k, v, RemovalCause.EXPLICIT)); - return null; - }); - } - }); - } - - @Override - public Collection getPresentKeys() { - return new LinkedHashSet<>(level2.asMap().keySet()); - } - - /** Essentially a 'NonAtomicInteger' */ - private static class Latch { - // A flag to indicate that removal of the corresponding entry from keyToSynchronizer needs to be prevented - // because another thread already started reusing this latch - volatile int numWaitingThreads = 1; - - Latch inc() { ++numWaitingThreads; return this; } - Latch dec() { --numWaitingThreads; return this; } - int get() { return numWaitingThreads; } - - @Override - public String toString() { - return "Latch " + System.identityHashCode(this) + " has "+ numWaitingThreads + " threads waiting"; - } - } -} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/Holder.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/Holder.java new file mode 100644 index 00000000000..9f9e6ac2a3e --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/Holder.java @@ -0,0 +1,48 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.claimingcache; + +import java.util.Objects; + +/** + * Helper class to set and get a reference within a lambda. + * A bit nicer than using an array with a single item. + */ +class Holder { + private T value; + + public void set(T value) { this.value = value; } + public T get() { return value; } + + private Holder(T value) { + this.value = value; + } + + public static Holder of(T value) { + return new Holder<>(value); + } + + @Override + public String toString() { + return Objects.toString(value); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/GroupedBatchImpl.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/PredicateContains.java similarity index 60% rename from jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/GroupedBatchImpl.java rename to jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/PredicateContains.java index 012e24a070e..6ac3b237372 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/GroupedBatchImpl.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/PredicateContains.java @@ -19,35 +19,31 @@ * SPDX-License-Identifier: Apache-2.0 */ -package org.apache.jena.sparql.service.enhancer.impl; +package org.apache.jena.sparql.service.enhancer.claimingcache; -/** - * Implementation that combines a batch with a group key. - */ -public class GroupedBatchImpl, V> - implements GroupedBatch +import java.util.Collection; +import java.util.Objects; +import java.util.function.Predicate; + +/** Predicate to test for containment in a collection. */ +public class PredicateContains + implements Predicate { - protected G groupKey; - protected Batch batch; + private Collection collection; - public GroupedBatchImpl(G groupKey, Batch batch) { + public PredicateContains(Collection collection) { super(); - this.groupKey = groupKey; - this.batch = batch; - } - - @Override - public G getGroupKey() { - return groupKey; + this.collection = Objects.requireNonNull(collection); } @Override - public Batch getBatch() { - return batch; + public boolean test(T t) { + boolean result = collection.contains(t); + return result; } @Override public String toString() { - return "GroupedBatchImpl [groupKey=" + groupKey + ", batch=" + batch + "]"; + return collection.toString(); } -} \ No newline at end of file +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/PredicateRange.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/PredicateRange.java new file mode 100644 index 00000000000..376771c99c7 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/PredicateRange.java @@ -0,0 +1,69 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.claimingcache; + +import java.io.Serializable; +import java.util.Objects; +import java.util.function.Predicate; + +import com.google.common.collect.Range; + +/** Predicate to match by a range. */ +public class PredicateRange> + implements Predicate, Serializable +{ + private static final long serialVersionUID = 1L; + private final Range range; + + public PredicateRange(Range range) { + super(); + this.range = Objects.requireNonNull(range); + } + + @Override + public boolean test(T t) { + boolean result = range.contains(t); + return result; + } + + @Override + public String toString() { + return range.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(range); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PredicateRange other = (PredicateRange) obj; + return Objects.equals(range, other.range); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/PredicateRangeSet.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/PredicateRangeSet.java new file mode 100644 index 00000000000..85c0eba1a0f --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/PredicateRangeSet.java @@ -0,0 +1,70 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.claimingcache; + +import java.io.Serializable; +import java.util.Objects; +import java.util.function.Predicate; + +import com.google.common.collect.RangeSet; + +/** Predicate to match by a range set. */ +public class PredicateRangeSet> + implements Predicate, Serializable +{ + private static final long serialVersionUID = 1L; + + private final RangeSet rangeSet; + + public PredicateRangeSet(RangeSet rangeSet) { + super(); + this.rangeSet = Objects.requireNonNull(rangeSet); + } + + @Override + public boolean test(T t) { + boolean result = rangeSet.contains(t); + return result; + } + + @Override + public String toString() { + return rangeSet.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(rangeSet); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + PredicateRangeSet other = (PredicateRangeSet) obj; + return Objects.equals(rangeSet, other.rangeSet); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/PredicateTrue.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/PredicateTrue.java new file mode 100644 index 00000000000..78fe3e16f66 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/PredicateTrue.java @@ -0,0 +1,47 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.claimingcache; + +import java.util.function.Predicate; + +public class PredicateTrue + implements Predicate +{ + private static final Predicate INSTANCE = new PredicateTrue<>(); + + private PredicateTrue() {} + + @SuppressWarnings("unchecked") + public static Predicate get() { + return (Predicate) INSTANCE; + } + + @Override + public String toString() { + return "TRUE"; + } + + @Override + public boolean test(T t) { + return true; + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/Ref.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/Ref.java index 1c82431b5e5..3316406d218 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/Ref.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/Ref.java @@ -64,21 +64,21 @@ public interface Ref T get(); /** - * Return the object on which reference acquisition, release and the close action - * are synchronized on. + * Return a consumer that can synchronize the running of actions. + * Reference acquisition, release and the close action need to be synchronized on. */ - Object getSynchronizer(); - - /** - * Acquire a new reference with a given comment object - * Acquiration fails if isAlive() returns false - */ - Ref acquire(Object purpose); + Synchronizer getSynchronizer(); default Ref acquire() { return acquire(null); } + /** + * Acquire a new reference with a given comment object + * Acquisition fails if isAlive() returns false + */ + Ref acquire(Object comment); + /** * A reference may itself be closed, but references to it may keep it alive * @@ -91,7 +91,7 @@ default Ref acquire() { */ boolean isClosed(); - // Overrides the throws declaration of Autoclose + /** Closes this Ref. Overrides the throws declaration of Autoclose */ @Override void close(); diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefDelegate.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefDelegate.java index eaeb1cc976c..110c14dcd7e 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefDelegate.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefDelegate.java @@ -58,7 +58,7 @@ default void close() { } @Override - default Object getSynchronizer() { + default Synchronizer getSynchronizer() { return getDelegate().getSynchronizer(); } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefDelegateBase.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefDelegateBase.java index 92592908e0f..9a48495c134 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefDelegateBase.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefDelegateBase.java @@ -35,4 +35,4 @@ public RefDelegateBase(R delegate) { public R getDelegate() { return delegate; } -} \ No newline at end of file +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefFuture.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefFuture.java index 573d3bc95ee..41f50bc752c 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefFuture.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefFuture.java @@ -43,12 +43,14 @@ default T await() { @Override RefFuture acquire(); + @Override + RefFuture acquire(Object comment); + /** Create a sub-reference to a transformed value of the CompletableFuture */ // Result must be closed by caller default RefFuture acquireTransformed(Function transform) { RefFuture acquired = this.acquire(); - Object synchronizer = acquired.getSynchronizer(); - + Synchronizer synchronizer = acquired.getSynchronizer(); CompletableFuture future = acquired.get().thenApply(transform); RefFuture result = RefFutureImpl.wrap(RefImpl.create(future, synchronizer, acquired::close)); return result; diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefFutureImpl.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefFutureImpl.java index 0984f3932a2..e004df52764 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefFutureImpl.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefFutureImpl.java @@ -43,7 +43,12 @@ public RefFutureImpl(Ref> delegate) { @Override public RefFuture acquire() { - return wrap(getDelegate().acquire()); + return acquire(null); + } + + @Override + public RefFuture acquire(Object comment) { + return wrap(getDelegate().acquire(comment)); } /** @@ -65,7 +70,7 @@ public static RefFuture fromRef(Ref ref) { } /** Create a ref that upon close cancels the future or closes the ref when it is available s*/ - public static RefFuture fromFuture(CompletableFuture> future, Object synchronizer) { + public static RefFuture fromFuture(CompletableFuture> future, Synchronizer synchronizer) { return wrap(RefImpl.create(future.thenApply(Ref::get), synchronizer, () -> cancelFutureOrCloseRef(future), null)); } @@ -101,4 +106,21 @@ public static void cancelFutureOrCloseValue(CompletableFuture future, Con logger.warn("Exception raised during close", e); } } -} \ No newline at end of file + + @Override + public String toString() { + CompletableFuture future = this.get(); + String status; + if (future.isDone()) { + try { + T value = future.get(); + status = "" + value; + } catch (InterruptedException | ExecutionException e) { + status = "failed: " + e; + } + } else { + status = "pending..."; + } + return "RefFuture [" + status + "]"; + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefImpl.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefImpl.java index 7aa5e091566..a3ed300fedc 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefImpl.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/RefImpl.java @@ -62,7 +62,7 @@ public class RefImpl * closing a reference removes the map entry before it can be accessed and conversely, * synchronizing on the map prevents the reference from becoming released. */ - protected Object synchronizer; + protected Synchronizer synchronizer; protected Object comment; // An attribute which can be used for debugging reference chains protected RefImpl parent; @@ -84,14 +84,14 @@ public class RefImpl public RefImpl( RefImpl parent, T value, - Object synchronizer, + Synchronizer synchronizer, AutoCloseable releaseAction, Object comment) { super(); this.parent = parent; this.value = value; this.releaseAction = releaseAction; - this.synchronizer = synchronizer == null ? this : synchronizer; + this.synchronizer = synchronizer == null ? this::defaultSynchronizer : synchronizer; this.comment = comment; if (traceAcquisitions) { @@ -99,6 +99,13 @@ public RefImpl( } } + /** Default synchronizer runs the action while synchronizing on 'this' */ + protected void defaultSynchronizer(Runnable action) { + synchronized (this) { + action.run(); + } + } + /** * Note: Actually this method should be replaced with an approach using Java 9 Cleaner * however I couldn't get the cleaner to run. @@ -108,7 +115,7 @@ public RefImpl( protected void finalize() throws Throwable { try { if (!isClosed) { - synchronized (synchronizer) { + synchronizer.accept(() -> { if (!isClosed) { String msg = "Ref released by GC rather than user logic - indicates resource leak." + "Acquired at " + StackTraceUtils.toString(acquisitionStackTrace); @@ -116,7 +123,7 @@ protected void finalize() throws Throwable { close(); } - } + }); } } finally { super.finalize(); @@ -128,7 +135,7 @@ public Object getComment() { } @Override - public Object getSynchronizer() { + public Synchronizer getSynchronizer() { return synchronizer; } @@ -147,27 +154,34 @@ public T get() { return value; } + /** + * @param comment A comment to attach to the acquired reference. + */ @Override public Ref acquire(Object comment) { - synchronized (synchronizer) { + Holder> result = Holder.of(null); + Runnable action = () -> { if (!isAlive()) { String msg = "Cannot acquire from a reference with status 'isAlive=false'" + "\nClose triggered at: " + StackTraceUtils.toString(closeTriggerStackTrace); throw new RuntimeException(msg); } - // A bit of ugliness to allow the reference to release itself - @SuppressWarnings("rawtypes") - Ref[] tmp = new Ref[1]; - tmp[0] = new RefImpl<>(this, value, synchronizer, () -> release(tmp[0]), comment); + // A bit of ugliness to allow the reference to release itself. + Holder> tmp = Holder.of(null); + tmp.set(new RefImpl<>(this, value, synchronizer, () -> { + Ref ref = tmp.get(); + release(ref); + }, comment)); - @SuppressWarnings("unchecked") - Ref result = tmp[0]; - childRefs.put(result, comment); + result.set(tmp.get()); + childRefs.put(result.get(), comment); ++activeChildRefs; - //activeChildRefs.incrementAndGet(); - return result; - } + }; + + synchronizer.accept(action); + + return result.get(); } protected void release(Object childRef) { @@ -191,7 +205,7 @@ public boolean isAlive() { @Override public void close() { - synchronized (synchronizer) { + Runnable action = () -> { if (isClosed) { String msg = "Reference was already closed." + "\nReleased at: " + StackTraceUtils.toString(closeStackTrace) + @@ -208,7 +222,8 @@ public void close() { checkRelease(); } - } + }; + synchronizer.accept(action); } protected void checkRelease() { @@ -228,20 +243,20 @@ protected void checkRelease() { } } - public static Ref fromCloseable(T value, Object synchronizer) { + public static Ref fromCloseable(T value, Synchronizer synchronizer) { return create(value, synchronizer, value); } /** Create method where the close action is created from a provided lambda that accepts the value */ - public static Ref create2(T value, Object synchronizer, Consumer closer) { + public static Ref create2(T value, Synchronizer synchronizer, Consumer closer) { return create(value, synchronizer, () -> closer.accept(value), null); } - public static Ref create(T value, Object synchronizer, AutoCloseable releaseAction) { + public static Ref create(T value, Synchronizer synchronizer, AutoCloseable releaseAction) { return create(value, synchronizer, releaseAction, null); } - public static Ref create(T value, Object synchronizer, AutoCloseable releaseAction, Object comment) { + public static Ref create(T value, Synchronizer synchronizer, AutoCloseable releaseAction, Object comment) { return new RefImpl<>(null, value, synchronizer, releaseAction, comment); } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/Synchronizer.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/Synchronizer.java new file mode 100644 index 00000000000..f67b06e9cfb --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/Synchronizer.java @@ -0,0 +1,54 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.claimingcache; + +import java.util.function.Consumer; + +/** + * Abstracts synchronization for running actions atomically. + * Examples for typical implementations: + *
+ * // Example 1
+ * synchronized (object) {
+ *   action.run();
+ * }
+ *
+ * // Example 2
+ * concurrentHashMap.compute(key, (k, v) -> {
+ *   action.run();
+ *   retun null;
+ * });
+ *
+ * // Example 3
+ * lock.lock();
+ * try {
+ *   action.run();
+ * } finally {
+ *   lock.unlock();
+ * }
+ * 
+ * + */ +public interface Synchronizer + extends Consumer +{ +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/SynchronizerMap.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/SynchronizerMap.java new file mode 100644 index 00000000000..01c995e5de8 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/SynchronizerMap.java @@ -0,0 +1,126 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.claimingcache; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * A helper to synchronize actions on keys. + * This class maps keys to proxy objects for synchronization and + * provides methods to remove the proxy objects when they are no longer needed. + * + * @param The key type. + */ +public class SynchronizerMap { + private ConcurrentHashMap map = new ConcurrentHashMap<>(); + + public static class SynchronizerImpl + implements Synchronizer + { + private SynchronizerMap map; + + /** The id. This is the value of the counter when the key was acquired. */ + private final int id; + private final K key; + private final VolatileCounter counter; + + public SynchronizerImpl(SynchronizerMap map, K key, VolatileCounter counter, int id) { + super(); + this.map = map; + this.key = key; + this.counter = counter; + this.id = id; + } + + @Override + public void accept(Runnable action) { + synchronized (counter) { + action.run(); + } + } + + public void clearEntryIfZero() { + map.clearEntryIfZero(key); + } + + private void dec() { + counter.dec(); + } + + @Override + public String toString() { + return "Synchronizer on " + System.identityHashCode(map) + ", " + + String.join(", ", "id: " + id, "key: " + key, "current count: " + counter.get()); + } + } + + public T compute(K key, Function, T> handler) { + SynchronizerImpl synchronizer = acquire(key); + Holder result = Holder.of(null); + synchronizer.accept(() -> { + // Decrement the refcount of the synchronizer. Does not clear the key's proxy object. + synchronizer.dec(); + + T r = handler.apply(synchronizer); + result.set(r); + }); + return result.get(); + } + + private SynchronizerImpl acquire(K key) { + Holder id = Holder.of(null); + VolatileCounter counter = map.compute(key, (k, before) -> { + VolatileCounter r = before == null ? new VolatileCounter(1) : before.inc(); + id.set(r.get()); // Atomically expose the current value of the counter + return r; + }); + SynchronizerImpl result = new SynchronizerImpl<>(this, key, counter, id.get()); + // System.out.println("Acquired: " + result); + return result; + } + + private void clearEntryIfZero(K key) { + map.compute(key, this::clearEntryIfZero); + } + + /** This method is run atomically */ + private VolatileCounter clearEntryIfZero(K key, VolatileCounter counter) { + if (counter == null) { + throw new IllegalStateException("No counter for key " + key); + } + int count = counter.get(); + if (counter.get() < 0) { + throw new IllegalStateException("Negative count for key " + key + ": " + count); + } + + VolatileCounter result = count == 0 + ? null + : counter; + +// if (count == 0) { +// System.out.println("Cleared entry for key " + key); +// } + + return result; + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/VolatileCounter.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/VolatileCounter.java new file mode 100644 index 00000000000..55d68b7d3be --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/claimingcache/VolatileCounter.java @@ -0,0 +1,44 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.claimingcache; + +/** + * A counter that can be accessed by multiple threads. + * Synchronization must be ensured extrinsically, such as using synchronized blocks or within + * ConcurrentHashMap.compute. + */ +class VolatileCounter { + private volatile int value ; + + public VolatileCounter(int value) { + this.value = value; + } + + public VolatileCounter inc() { ++value; return this; } + public VolatileCounter dec() { --value; return this; } + public int get() { return value; } + + @Override + public String toString() { + return "Volatile counter " + System.identityHashCode(this) + " has value " + value; + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/AutoLock.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/AutoLock.java new file mode 100644 index 00000000000..495a80484ba --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/AutoLock.java @@ -0,0 +1,46 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.concurrent; + +import java.util.concurrent.locks.Lock; + +public class AutoLock implements AutoCloseable { + private final Lock lock; + + private AutoLock(Lock lock) { + this.lock = lock; + } + + /** + * Immediately attempts to acquire the lock and returns + * an auto-closeable AutoLock instance for use with try-with-resources. + */ + public static AutoLock lock(Lock lock) { + lock.lock(); + return new AutoLock(lock); + } + + @Override + public void close() { + lock.unlock(); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/CloseShieldExecutorService.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/CloseShieldExecutorService.java new file mode 100644 index 00000000000..65abcb3ed41 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/CloseShieldExecutorService.java @@ -0,0 +1,101 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.concurrent; + +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.google.common.util.concurrent.ForwardingExecutorService; + +/** Wrapper for an executor service. Overrides the {@link #shutdown()} and {@link #shutdownNow()} with no-ops. */ +public class CloseShieldExecutorService + extends ForwardingExecutorService { + + protected X delegate; + protected AtomicBoolean isShutDown = new AtomicBoolean(); + + public CloseShieldExecutorService(X delegate) { + super(); + this.delegate = delegate; + } + + @Override + protected X delegate() { + return delegate; + } + + protected void checkOpen() { + if (isShutdown()) { + throw new RejectedExecutionException("Executor service is already shut down"); + } + } + + @Override + public Future submit(Callable task) { + checkOpen(); + return super.submit(task); + } + + @Override + public Future submit(Runnable task) { + checkOpen(); + return super.submit(task); + } + + @Override + public Future submit(Runnable task, T result) { + checkOpen(); + return super.submit(task, result); + } + + @Override + public void shutdown() { + isShutDown.set(true); + } + + /** Immediately returns because only the view pretends to shut down. */ + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return true; + } + + @Override + public List shutdownNow() { + isShutDown.set(true); + return List.of(); + } + + @Override + public boolean isShutdown() { + return isShutDown.get() || super.isShutdown(); + } + + @Override + public boolean isTerminated() { + return super.isTerminated(); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/ExecutorServicePool.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/ExecutorServicePool.java new file mode 100644 index 00000000000..58ea3ee9323 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/ExecutorServicePool.java @@ -0,0 +1,330 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.concurrent; + +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.jena.sparql.service.enhancer.util.IdPool; +import org.apache.jena.sparql.service.enhancer.util.LinkedList; +import org.apache.jena.sparql.service.enhancer.util.LinkedList.LinkedListNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.util.concurrent.ForwardingExecutorService; + +/** + * A factory for single thread executors. The returned executor services are wrappers. + * You must eventually call {@link ExecutorService#shutdown()} or {@link ExecutorService#shutdownNow()} + * on the wrapper in order to return the underlying executor service back to the pool. + */ +public class ExecutorServicePool { + + private static final Logger logger = LoggerFactory.getLogger(ExecutorServicePool.class); + + private class ExecutorServiceWithKey + extends ForwardingExecutorService + { + private final LinkedListNode node; + private ExecutorService delegate; + + public ExecutorServiceWithKey(ExecutorService delegate, LinkedListNode node) { + super(); + this.delegate = delegate; + this.node = node; + // TODO executorId could be copied because its final + } + + @Override + protected ExecutorService delegate() { + return delegate; + } + + public LinkedListNode getNode() { + return node; + } + } + + /** This is the view implementation handed out to clients. */ + private class ExecutorServiceInternal + extends CloseShieldExecutorService + { + public ExecutorServiceInternal(ExecutorServiceWithKey delegate) { + super(delegate); + } + + @Override + public void shutdown() { + super.shutdown(); + giveBack(delegate); + } + + + @Override + public List shutdownNow() { + super.shutdownNow(); + giveBack(delegate); + return List.of(); + } + } + + // Shutting down the pool also shuts down all executors. + private final ConcurrentHashMap executorMap = new ConcurrentHashMap<>(); + + private final AtomicBoolean isShutdown = new AtomicBoolean(); + private final IdPool idPool = new IdPool(); + private final long idleTimeout; + private final int maxIdleExecutors; + + private final boolean isDaemon = true; + + // private ScheduledExecutorService cleaner; + private Timer timer; + + private volatile boolean isCleanupScheduled = false; + + private void scheduleCleanup() { + synchronized (actions) { + if (!isCleanupScheduled) { + if (logger.isDebugEnabled()) { + logger.debug("Cleanup of idle executors scheduled in {} ms", idleTimeout); + } + + if (timer == null) { + timer = new Timer(isDaemon); + } + + isCleanupScheduled = true; + timer.schedule(new TimerTask() { + @Override + public void run() { + doCleanup(); + } + }, idleTimeout); + // If a task has been scheduled AND not yet executed then do nothing; otherwise schedule a new task + } else { + // if (logger.isWarnEnabled()) { + // logger.warn("Request for cleanup of idle executors ignored because a pending action was already scheduled."); + // } + } + } + } + + private class ExecutorState { + int executorId; + ExecutorServiceWithKey executorService; + + /** When the executor has become idle. Only needs to be set before insert into the idleList. */ + long idleTimestamp; + } + + /** + * A doubly linked list to keep track of idle executors in the ExecutorServicePool. + * Each executor keeps a reference to a single node of this list. + * + * If the executor becomes busy then it unlinks itself from the list. + * If the executor becomes idle then it appends itself to the end of this list with its idle timestamp. + * + * Consequently, the executors that have been idle longest are at the beginning of the list. + * The cleanup task only has to release the idle executors at the beginning of the list. + * The cleanup task can stop when encountering an executor whose idle time is too recent. + */ + private LinkedList actions = new LinkedList<>(); + + public ExecutorServicePool() { + this(0, 0); + } + + public ExecutorServicePool(long idleTimeout, int maxIdleExecutors) { + super(); + this.idleTimeout = idleTimeout; + this.maxIdleExecutors = maxIdleExecutors; + } + + private void checkOpen() { + if (isShutdown.get()) { + throw new IllegalStateException("Executor pool has been shut down."); + } + } + + /** Request an executor (creates new if none is available). */ + public ExecutorService acquireExecutor() { + checkOpen(); + + // Attempt to get an executor from the idle list + ExecutorServiceWithKey backend = null; + synchronized (actions) { + LinkedListNode node; + node = actions.getFirstNode(); + if (node != null) { + // Synchronized unlinking of the node prevents accidental concurrent cleanup + node.unlink(); + backend = node.getValue().executorService; + } + } + + // If there was no idle executor then allocate a fresh one + if (backend == null) { + int executorId = idPool.acquire(); + // FIXME Make sure that the executor with the next free id is not shutting down while we try to claim it + // So newBackend should be synchronized with giveBack. + backend = executorMap.computeIfAbsent(executorId, this::newBackend); + } + + ExecutorServiceInternal result = new ExecutorServiceInternal(backend); + if (logger.isDebugEnabled()) { + logger.debug("Acquired executor #{}.", backend.getNode().getValue().executorId); + } + return result; + } + + protected ExecutorServiceWithKey newBackend(int executorId) { + ExecutorService core = createSingleThreadExecutor(executorId); + LinkedListNode node = actions.newNode(); + ExecutorServiceWithKey result = new ExecutorServiceWithKey(core, node); + ExecutorState action = new ExecutorState(); + action.executorId = executorId; + action.executorService = result; + node.setValue(action); + return result; + } + + protected ExecutorService createSingleThreadExecutor(int executorId) { + ThreadFactory namingThreadFactory = runnable -> { + Thread thread = new Thread(runnable); + thread.setName("single-thread-executor-" + executorId); + thread.setDaemon(isDaemon); // Daemon threads auto-shutdown with JVM + return thread; + }; + + ExecutorService executor = Executors.newSingleThreadExecutor(namingThreadFactory); + + return executor; + // Use MoreExecutors to ensure the executor auto-shuts down after idleTimeout +// ExecutorService result = idleTimeout >= 0 +// ? MoreExecutors.getExitingExecutorService((ThreadPoolExecutor)executor, idleTimeout, timeUnit) +// : MoreExecutors.getExitingExecutorService((ThreadPoolExecutor)executor); + +// return result; + } + + private void giveBack(ExecutorServiceWithKey executor) { + LinkedListNode node = executor.getNode(); + node.getValue().idleTimestamp = System.currentTimeMillis(); + // Note: Even if there are more than maxIdleExecutors executors right now then + // we still only clean them up after the idle delay. + synchronized (actions) { + node.moveToEnd(); + } + scheduleCleanup(); + } + + /** Releases the executor (allows custom behavior if needed). Called from the cleanupTask. */ + private void releaseExecutor(ExecutorServiceWithKey executor, boolean updateExecutorMap) { + LinkedListNode node = executor.getNode(); + int executorId = node.getValue().executorId; + node.unlink(); + if (updateExecutorMap) { + executorMap.remove(executorId); + } + executor.shutdown(); + idPool.giveBack(executorId); + if (logger.isDebugEnabled()) { + logger.debug("Releasing executor #{}.", executorId); + } + } + + /** Shutdown all executors in the pool; pool should no longer be used then anymore. */ + public void shutdownAll() { + if (isShutdown.compareAndSet(false, true)) { + synchronized (actions) { + if (timer != null) { + timer.cancel(); + } + for (ExecutorServiceWithKey executor : executorMap.values()) { + releaseExecutor(executor, false); + } + executorMap.clear(); + } + } + } + + private void doCleanup() { + synchronized (actions) { + isCleanupScheduled = false; + if (logger.isDebugEnabled()) { + logger.debug("Cleanup of idle service executors starting."); + } + int cleanupCount = 0; + LinkedListNode node = actions.getFirstNode(); + long delta = -1; + if (node != null) { + long currentTime = System.currentTimeMillis(); + while(node != null) { + ExecutorState action = node.getValue(); + long timestamp = action.idleTimestamp; + delta = currentTime - timestamp; + if (delta >= idleTimeout || actions.size() > maxIdleExecutors) { + ++cleanupCount; + releaseExecutor(action.executorService, true); + delta = -1; + } else { + break; + } + node = node.getNext(); + } + } + + if (logger.isDebugEnabled()) { + logger.debug("Cleanup of idle service executors done - {} idle executors released.", cleanupCount); + } + + if (delta >= 0) { + // timer.schedule(this, delta); + scheduleCleanup(); + } + } + } + + public static void main(String[] args) throws Exception { + ExecutorServicePool pool = new ExecutorServicePool(); + + ExecutorService es0 = pool.acquireExecutor(); + es0.submit(() -> System.out.println(Thread.currentThread().getName() + " says hi!")); + + // Thread.sleep(5000); + + ExecutorService es1 = pool.acquireExecutor(); + es1.submit(() -> System.out.println(Thread.currentThread().getName() + " says hello!")); + es1.shutdown(); + + es0.shutdown(); + + pool.shutdownAll(); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/LockWrapper.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/LockWrapper.java new file mode 100644 index 00000000000..066f5ba8b38 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/LockWrapper.java @@ -0,0 +1,62 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.concurrent; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; + +public abstract class LockWrapper + implements Lock +{ + protected abstract Lock getDelegate(); + + @Override + public void lock() { + getDelegate().lock(); + } + + @Override + public void lockInterruptibly() throws InterruptedException { + getDelegate().lockInterruptibly(); + } + + @Override + public boolean tryLock() { + return getDelegate().tryLock(); + } + + @Override + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + return getDelegate().tryLock(); + } + + @Override + public void unlock() { + getDelegate().unlock(); + } + + @Override + public Condition newCondition() { + return getDelegate().newCondition(); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/ReadWriteLockModular.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/ReadWriteLockModular.java new file mode 100644 index 00000000000..67751b98c87 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/concurrent/ReadWriteLockModular.java @@ -0,0 +1,49 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.concurrent; + +import java.util.Objects; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReadWriteLock; + +public class ReadWriteLockModular + implements ReadWriteLock +{ + protected Lock readLock; + protected Lock writeLock; + + public ReadWriteLockModular(Lock readLock, Lock writeLock) { + super(); + this.readLock = Objects.requireNonNull(readLock); + this.writeLock = Objects.requireNonNull(writeLock); + } + + @Override + public Lock readLock() { + return readLock; + } + + @Override + public Lock writeLock() { + return writeLock; + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/example/ServiceCachingExamples.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/example/ServiceCachingExamples.java index 72951c9d08d..c5700844a03 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/example/ServiceCachingExamples.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/example/ServiceCachingExamples.java @@ -24,8 +24,6 @@ import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; -import com.google.common.base.Stopwatch; - import org.apache.jena.atlas.logging.LogCtl; import org.apache.jena.query.QueryExecution; import org.apache.jena.query.QueryExecutionFactory; @@ -39,9 +37,13 @@ import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerConstants; import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerInit; +import com.google.common.base.Stopwatch; + /** Examples for setting up and using SERVICE caching */ public class ServiceCachingExamples { + // This logger is initialized here only for the sake of examples. + // It will override any prior logger configuration. static { LogCtl.setLogging(); } public static void main(String[] args) { diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/example/ServiceConcurrentExample.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/example/ServiceConcurrentExample.java new file mode 100644 index 00000000000..2ed2f6a51cb --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/example/ServiceConcurrentExample.java @@ -0,0 +1,139 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.example; + +import org.apache.commons.lang3.time.StopWatch; +import org.apache.jena.atlas.logging.LogCtl; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.ResultSet; +import org.apache.jena.query.ResultSetFormatter; +import org.apache.jena.sparql.algebra.Table; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.core.DatasetGraphFactory; +import org.apache.jena.sparql.exec.QueryExec; +import org.apache.jena.sparql.resultset.ResultsFormat; +import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerInit; +import org.apache.jena.sparql.util.Context; + +/** + * This example demonstrates concurrent bulk retrieval from a remote endpoint. + *

+ * The central construct is explained as follows: + *

+ * SERVICE <loop+scoped:concurrent+20:bulk+50:cache:https://linkedgeodata.org/sparql> { ... }
+ * 
+ * + *
    + *
  • loop activates "for-each"-mode: + * Each binding produced by the graph pattern before the SERVICE clause becomes an input binding. + * Conceptually, the SERVICE clause is evaluated w.r.t. each input binding. + *
  • + *
  • concurrent+10-25 indicates to partition the input to 10 threads with 25 bindings assigned to each thread.
  • + *
  • bulk+25 indicates to group 25 input bindings into a single request. + * Note, that concurrent must appear before bulk so that bulk is executed + * within each partition created by concurrent.
  • + *
  • cache> instructs to cache the output bindings produced by the service pattern (including the service IRI) with each input binding.
  • + *
+ * + *

+ * + * Notes on loop+scoped: + *

+ * The scoped option causes loop to only substitute in-scope variables of the SERVICE pattern. + * Without scoped, loop would replace variables regardless of their scope - i.e. also variables nested in sub-queries. + * The scoped option aligns loop closer with the semantics of SPARQL's LATERAL keyword. + * + */ +public class ServiceConcurrentExample { + static { LogCtl.setJavaLogging(); } + + public static void main(String[] args) { + String endpointUrl = "https://data.aksw.org/coypu"; + + // Number of threads in addition to the main thread. + int numThreads = 10; + + // Bindings per batch. + // If the number is too high the HTTP requests may fail because they become too large. + int numBindingsPerBatch = 25; + + // How often to repeat the query + int numRepeats = 3; + boolean showTableOnlyOnFirstIteration = true; + boolean showTable = true; + boolean showFinalQuery = true; + + String queryStr = """ + PREFIX owl: + SELECT * { + # Fetch 1000 classes from the remote endpoint + SERVICE <{E}> { + SELECT * { + ?t a owl:Class . + FILTER(isIRI(?t)) + } LIMIT 1000 + } + + # For each class fetch 2 instances. Use T threads each with a batch of B classes. + SERVICE { + # LATERAL { SERVICE <{E}> { + SELECT * { ?s a ?t } LIMIT 2 + # } } + } + } + """ + .replace("{E}", endpointUrl) + .replace("{T}", Integer.toString(numThreads)) + .replace("{B}", Integer.toString(numBindingsPerBatch)) + ; + + DatasetGraph dsg = DatasetGraphFactory.create(); + + // Enable loop on the data set (registers a pre-processing step to Jena's default optimizer). + Context cxt = dsg.getContext(); + ServiceEnhancerInit.wrapOptimizer(cxt); + + // Configure a bigger cache as needed. + // ServiceResponseCache.set(cxt, new ServiceResponseCache(10000, 10000, 15)); + + Query query = QueryFactory.create(queryStr); + + for (int i = 0; i < numRepeats; ++i) { + StopWatch sw = StopWatch.createStarted(); + Table table = QueryExec.dataset(dsg).query(query).table(); + sw.stop(); + + if (showTable && !(showTableOnlyOnFirstIteration && i != 0)) { + ResultSetFormatter.output(System.out, ResultSet.adapt(table.toRowSet()), ResultsFormat.TEXT); + } + + System.out.println("Fetched " + table.size() + " rows in " + sw + "."); + System.out.println(); + + if (showFinalQuery && i + 1 == numRepeats) { + System.out.println("Query:"); + System.out.println(query); + } + } + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/example/ServicePluginExamples.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/example/ServicePluginExamples.java index df12985f56d..27efd2852ca 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/example/ServicePluginExamples.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/example/ServicePluginExamples.java @@ -36,13 +36,18 @@ public static void main(String[] args) { customLinearJoin(DatasetFactory.empty()); } + /** + * This example shows how to programmatically enable + * {@code SERVICE } using + * {@link ServiceEnhancerInit#wrapOptimizer(Context)}. + */ public static void customLinearJoin(Dataset dataset) { Context cxt = ARQ.getContext().copy(); ServiceEnhancerInit.wrapOptimizer(cxt); String queryStr = "SELECT * {\n" + " BIND( AS ?s)\n" - + " SERVICE {\n" + + " SERVICE {\n" + " { BIND(?s AS ?x) } UNION { BIND(?s AS ?y) }\n" + " }\n" + "}"; diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/Batch.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/Batch.java index 5a206111e36..a12ed25a9a4 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/Batch.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/Batch.java @@ -18,7 +18,6 @@ * * SPDX-License-Identifier: Apache-2.0 */ - package org.apache.jena.sparql.service.enhancer.impl; import java.util.NavigableMap; diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchImpl.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchImpl.java index 6ded7c1a24a..369816cde50 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchImpl.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchImpl.java @@ -33,6 +33,7 @@ public class BatchImpl, T> implements Batch { + /** The first key of the batch. Tracked separately because {@link #items} may be empty. */ protected K firstKey; protected DiscreteDomain discreteDomain; protected NavigableMap items; @@ -49,11 +50,19 @@ public BatchImpl(K firstKey, DiscreteDomain discreteDomain) { } public static Batch forInteger() { - return new BatchImpl<>(0, DiscreteDomain.integers()); + return forInteger(0); + } + + public static Batch forInteger(int startIndex) { + return new BatchImpl<>(startIndex, DiscreteDomain.integers()); } public static Batch forLong() { - return new BatchImpl<>(0l, DiscreteDomain.longs()); + return forLong(0l); + } + + public static Batch forLong(long startIndex) { + return new BatchImpl<>(startIndex, DiscreteDomain.longs()); } /** diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchQueryRewriteResult.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchQueryRewriteResult.java index 172cc7f70c9..c5145e51dd0 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchQueryRewriteResult.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchQueryRewriteResult.java @@ -54,4 +54,4 @@ public Map getRenames() { public String toString() { return "BatchQueryRewriteResult [op=" + op + ", renames=" + renames + "]"; } -} \ No newline at end of file +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchQueryRewriter.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchQueryRewriter.java index deabf3ca76f..b2181044cec 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchQueryRewriter.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchQueryRewriter.java @@ -33,7 +33,6 @@ import java.util.Optional; import java.util.Set; -import org.apache.jena.atlas.logging.Log; import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; import org.apache.jena.query.Query; @@ -45,13 +44,17 @@ import org.apache.jena.sparql.algebra.op.OpOrder; import org.apache.jena.sparql.algebra.op.OpSlice; import org.apache.jena.sparql.algebra.op.OpUnion; +import org.apache.jena.sparql.core.Substitute; import org.apache.jena.sparql.core.Var; import org.apache.jena.sparql.engine.binding.Binding; import org.apache.jena.sparql.engine.main.QC; import org.apache.jena.sparql.expr.ExprVar; import org.apache.jena.sparql.expr.NodeValue; import org.apache.jena.sparql.graph.NodeTransformLib; +import org.apache.jena.sparql.service.enhancer.impl.util.AssertionUtils; import org.apache.jena.sparql.service.enhancer.impl.util.BindingUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Rewriter for instantiating a query such that a list of initial bindings are injected. @@ -68,6 +71,20 @@ * */ public class BatchQueryRewriter { + + public enum SubstitutionStrategy { + /** Approach using {@link QC#substitute(Op, Binding)}. */ + SUBSTITUTE, + + /** + * Experimental approach using {@link Substitute#inject(Op, Binding)}. + * Seems less reliable than SUBSTITUTE. + */ + INJECT + } + + private static final Logger logger = LoggerFactory.getLogger(BatchQueryRewriter.class); + protected OpServiceInfo serviceInfo; protected Var idxVar; @@ -82,10 +99,11 @@ public class BatchQueryRewriter { */ protected boolean orderRetainingUnion; - /** Whether to omit the end marker */ protected boolean omitEndMarker; + protected SubstitutionStrategy substitutionStrategy; + /** Constant to mark end of a batch (could also be dynamically set to one higher then the idx in a batch) */ static int REMOTE_END_MARKER = 1000000000; static NodeValue NV_REMOTE_END_MARKER = NodeValue.makeInteger(REMOTE_END_MARKER); @@ -100,13 +118,15 @@ public static boolean isRemoteEndMarker(Integer id) { public BatchQueryRewriter(OpServiceInfo serviceInfo, Var idxVar, boolean sequentialUnion, boolean orderRetainingUnion, - boolean omitEndMarker) { + boolean omitEndMarker, SubstitutionStrategy substitutionStrategy) { super(); this.serviceInfo = serviceInfo; this.idxVar = idxVar; this.sequentialUnion = sequentialUnion; this.orderRetainingUnion = orderRetainingUnion; this.omitEndMarker = omitEndMarker; + + this.substitutionStrategy = Objects.requireNonNull(substitutionStrategy); } /** The index var used by this rewriter */ @@ -116,7 +136,7 @@ public Var getIdxVar() { public static Set seenVars(Collection> batchRequest) { Set result = new LinkedHashSet<>(); - batchRequest.forEach(br -> BindingUtils.addAll(result, br.getPartitionKey())); + batchRequest.forEach(br -> BindingUtils.addAll(result, br.partitionKey())); return result; } @@ -148,14 +168,14 @@ public BatchQueryRewriteResult rewrite(Batch> if (!omitEndMarker) { Op endMarker = OpExtend.create(OpLib.unit(), idxVar, NV_REMOTE_END_MARKER); - newOp = newOp == null ? endMarker : OpUnion.create(newOp, endMarker); + newOp = endMarker; } for (Entry> e : es) { PartitionRequest req = e.getValue(); long idx = e.getKey(); - Binding scopedBinding = req.getPartitionKey(); + Binding scopedBinding = req.partitionKey(); Set scopedBindingVars = BindingUtils.varsMentioned(scopedBinding); @@ -169,14 +189,17 @@ public BatchQueryRewriteResult rewrite(Batch> // Note: QC.substitute does not remove variables being substituted from projections // This may cause unbound variables to be projected - - op = QC.substitute(op, normedBinding); + op = switch (substitutionStrategy) { + case SUBSTITUTE -> QC.substitute(op, normedBinding); + //case INJECT -> Transformer.transform(TransformAssignToExtend.get(), Substitute.inject(op, normedBinding)); + case INJECT -> Substitute.inject(op, normedBinding); + }; // Relabel any blank nodes op = NodeTransformLib.transform(node -> relabelBnode(node, idx), op); - long o = req.hasOffset() ? req.getOffset() : Query.NOLIMIT; - long l = req.hasLimit() ? req.getLimit() : Query.NOLIMIT; + long o = req.hasOffset() ? req.offset() : Query.NOLIMIT; + long l = req.hasLimit() ? req.limit() : Query.NOLIMIT; if (o != Query.NOLIMIT || l != Query.NOLIMIT) { op = new OpSlice(op, o, l); @@ -186,14 +209,25 @@ public BatchQueryRewriteResult rewrite(Batch> newOp = newOp == null ? op : OpUnion.create(op, newOp); } - if (orderNeeded) { newOp = new OpOrder(newOp, sortConditions); } - Query q = OpAsQuery.asQuery(newOp); - - Log.info(BatchQueryRewriter.class, "Rewritten bulk query: " + q); + if (logger.isInfoEnabled()) { + Query q = OpAsQuery.asQuery(newOp); + String str = q.toString(); + // Cut off strings unless assertions are enabled + String note = ""; + int len = str.length(); + if (!AssertionUtils.IS_ASSERT_ENABLED) { + int maxlen = 1024; + if (len > maxlen) { + str = str.subSequence(0, maxlen) + " ..."; + note = " (enable assertions using the -ea jvm option to see the full query)"; + } + } + logger.info("Rewritten bulk query has " + len + " characters" + note + ": " + str); + } // Add a rename for idxVar so that QueryIter.map does not omit it Map renames = new HashMap<>(serviceInfo.getVisibleSubOpVarsNormedToScoped()); diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchQueryRewriterBuilder.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchQueryRewriterBuilder.java index fccab372716..95d4dc57d61 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchQueryRewriterBuilder.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/BatchQueryRewriterBuilder.java @@ -19,9 +19,11 @@ * SPDX-License-Identifier: Apache-2.0 */ + package org.apache.jena.sparql.service.enhancer.impl; import org.apache.jena.sparql.core.Var; +import org.apache.jena.sparql.service.enhancer.impl.BatchQueryRewriter.SubstitutionStrategy; public class BatchQueryRewriterBuilder { protected OpServiceInfo serviceInfo; @@ -29,6 +31,7 @@ public class BatchQueryRewriterBuilder { protected boolean sequentialUnion; protected boolean orderRetainingUnion; protected boolean omitEndMarker; + protected SubstitutionStrategy substitutionStrategy; public BatchQueryRewriterBuilder(OpServiceInfo serviceInfo, Var idxVar) { super(); @@ -63,11 +66,24 @@ public BatchQueryRewriterBuilder setOmitEndMarker(boolean omitEndMarker) { return this; } + public BatchQueryRewriterBuilder setSubstitutionStrategy(SubstitutionStrategy substitutionStrategy) { + this.substitutionStrategy = substitutionStrategy; + return this; + } + + public SubstitutionStrategy getSubstitutionStrategy() { + return substitutionStrategy; + } + public static BatchQueryRewriterBuilder from(OpServiceInfo serviceInfo, Var idxVar) { return new BatchQueryRewriterBuilder(serviceInfo, idxVar); } public BatchQueryRewriter build() { - return new BatchQueryRewriter(serviceInfo, idxVar, sequentialUnion, orderRetainingUnion, omitEndMarker); + SubstitutionStrategy finalSubstitutionStrategy = substitutionStrategy == null + ? SubstitutionStrategy.SUBSTITUTE + : substitutionStrategy; + + return new BatchQueryRewriter(serviceInfo, idxVar, sequentialUnion, orderRetainingUnion, omitEndMarker, finalSubstitutionStrategy); } } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/Batcher.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/Batcher.java index 3a0fc9fda51..ce0386722ea 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/Batcher.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/Batcher.java @@ -21,14 +21,21 @@ package org.apache.jena.sparql.service.enhancer.impl; -import java.util.*; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; import java.util.Map.Entry; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.Optional; +import java.util.TreeMap; import java.util.function.Function; -import com.google.common.collect.AbstractIterator; - -import org.apache.jena.atlas.iterator.Iter; -import org.apache.jena.atlas.iterator.IteratorCloseable; +import org.apache.jena.atlas.io.IndentedWriter; +import org.apache.jena.atlas.lib.Lib; +import org.apache.jena.sparql.serializer.SerializationContext; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterator; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbstractAbortableIterator; /** * The batcher transform an iterator of input items into an iterator of batches. @@ -66,14 +73,14 @@ public Batcher(Function itemToGroupKey, int maxBulkSize, int maxOutOfBandI this.maxOutOfBandItemCount = maxOutOfBandItemCount; } - public IteratorCloseable> batch(IteratorCloseable inputIterator) { + public AbortableIterator> batch(AbortableIterator inputIterator) { return new IteratorGroupedBatch(inputIterator); } class IteratorGroupedBatch - extends AbstractIterator> implements IteratorCloseable> + extends AbstractAbortableIterator> { - protected IteratorCloseable inputIterator; + protected AbortableIterator inputIterator; /** The position of the inputIterator */ protected long inputIteratorOffset; @@ -84,18 +91,22 @@ class IteratorGroupedBatch // The outer navigable map has to lowest offset among all the group key's related batches protected Map>> groupToBatches = new HashMap<>(); - public IteratorGroupedBatch(IteratorCloseable inputIterator) { + public IteratorGroupedBatch(AbortableIterator inputIterator) { this(inputIterator, 0); } - public IteratorGroupedBatch(IteratorCloseable inputIterator, int inputIteratorOffset) { + public IteratorGroupedBatch(AbortableIterator inputIterator, int inputIteratorOffset) { super(); this.inputIterator = inputIterator; this.inputIteratorOffset = inputIteratorOffset; } + protected AbortableIterator getInput() { + return inputIterator; + } + @Override - protected GroupedBatch computeNext() { + protected GroupedBatch moveToNext() { // For the current result group key and corresponding batch determine how many out-of-band // items we have already consumed from the input iterator // Any item that does not contribute to the current result batch counts as out-of-band @@ -201,7 +212,7 @@ protected GroupedBatch computeNext() { Batch resultBatchTmp = nextBatchesIt.next(); nextBatchesIt.remove(); - result = new GroupedBatchImpl<>(resultGroupKey, resultBatchTmp); + result = new GroupedBatch<>(resultGroupKey, resultBatchTmp); } else { result = endOfData(); } @@ -209,8 +220,35 @@ protected GroupedBatch computeNext() { } @Override - public void close() { - Iter.close(inputIterator); + protected void closeIteratorActual() { + inputIterator.close(); + } + + @Override + protected void requestCancel() { + inputIterator.cancel(); + } + + @Override + public void output(IndentedWriter out) { + output(out, null); + } + + @Override + public void output(IndentedWriter out, SerializationContext sCxt) { + // Linear form. + if ( getInput() != null ) + // Closed + getInput().output(out, sCxt); + else + out.println("Closed"); + out.ensureStartOfLine(); + details(out, sCxt); + out.ensureStartOfLine(); + } + + protected void details(IndentedWriter out, SerializationContext sCxt) { + out.println(Lib.className(this)); } } } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ChainingServiceExecutorBulkCache.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ChainingServiceExecutorBulkCache.java index 082b92af7c8..319ed860e51 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ChainingServiceExecutorBulkCache.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ChainingServiceExecutorBulkCache.java @@ -23,7 +23,6 @@ import java.util.Optional; -import org.apache.jena.atlas.iterator.IteratorCloseable; import org.apache.jena.graph.Node; import org.apache.jena.sparql.algebra.op.OpService; import org.apache.jena.sparql.engine.ExecutionContext; @@ -31,11 +30,18 @@ import org.apache.jena.sparql.engine.binding.Binding; import org.apache.jena.sparql.service.bulk.ChainingServiceExecutorBulk; import org.apache.jena.sparql.service.bulk.ServiceExecutorBulk; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterator; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterators; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.QueryIterOverAbortableIterator; import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerConstants; import org.apache.jena.sparql.util.Context; -/** Do not register directly - use {@link ChainingServiceExecutorBulkServiceEnhancer} which gives more control over - * when to use this in a service executor chain */ +/** + * Do not register this class directly in a service executor chain. + * Instead, register {@link ChainingServiceExecutorBulkServiceEnhancer} which + * internally creates appropriately configured instances of this class + * during query execution. + */ public class ChainingServiceExecutorBulkCache implements ChainingServiceExecutorBulk { @@ -43,39 +49,45 @@ public class ChainingServiceExecutorBulkCache public static final int DFT_MAX_BULK_SIZE = 100; public static final int DFT_MAX_OUT_OUF_BAND_SIZE = 30; + public static final int DFT_MAX_CONCURRENT_SLOTS = 100; + + public static final int DFT_CONCURRENT_READAHEAD = 10000; + public static final int DFT_MAX_CONCURRENT_READAHEAD = 10000; + protected int bulkSize; protected CacheMode cacheMode; - public ChainingServiceExecutorBulkCache(int bulkSize, CacheMode cacheMode) { + protected int concurrentSlotCount; + protected long concurrentSlotReadaheadCount; + + public ChainingServiceExecutorBulkCache(int bulkSize, CacheMode cacheMode, int concurrentSlotCount, long concurrentSlotReadAheadCount) { super(); this.cacheMode = cacheMode; this.bulkSize = bulkSize; + this.concurrentSlotCount = concurrentSlotCount; + this.concurrentSlotReadaheadCount = concurrentSlotReadAheadCount; } @Override - public QueryIterator createExecution(OpService original, QueryIterator input, ExecutionContext execCxt, - ServiceExecutorBulk chain) { - + public QueryIterator createExecution(OpService original, QueryIterator input, ExecutionContext execCxt, ServiceExecutorBulk chain) { Context cxt = execCxt.getContext(); - // int bulkSize = cxt.getInt(InitServiceEnhancer.serviceBulkMaxBindingCount, DEFAULT_BULK_SIZE); + ServiceResponseCache serviceCache = CacheMode.OFF.equals(cacheMode) - ? null - : ServiceResponseCache.get(cxt); + ? null + : ServiceResponseCache.get(cxt); OpServiceInfo serviceInfo = new OpServiceInfo(original); - ServiceResultSizeCache resultSizeCache = Optional.ofNullable(cxt. - get(ServiceEnhancerConstants.serviceResultSizeCache)) - .orElseGet(ServiceResultSizeCache::new); + ServiceResultSizeCache resultSizeCache = Optional.ofNullable(cxt.get(ServiceEnhancerConstants.serviceResultSizeCache)) + .orElseGet(ServiceResultSizeCache::new); - OpServiceExecutorImpl opExecutor = new OpServiceExecutorImpl(serviceInfo.getOpService(), execCxt, chain); + OpServiceExecutorImpl opExecutor = new OpServiceExecutorImpl(serviceInfo.getOpService(), chain); int maxOutOfBandItemCount = cxt.getInt(ServiceEnhancerConstants.serviceBulkMaxOutOfBandBindingCount, DFT_MAX_OUT_OUF_BAND_SIZE); Batcher scheduler = new Batcher<>(serviceInfo::getSubstServiceNode, bulkSize, maxOutOfBandItemCount); - IteratorCloseable> inputBatchIterator = scheduler.batch(input); - - RequestExecutor exec = new RequestExecutor(opExecutor, serviceInfo, resultSizeCache, serviceCache, cacheMode, inputBatchIterator); + AbortableIterator> inputBatchIterator = scheduler.batch(AbortableIterators.adapt(input)); - return exec; + RequestExecutorBulkAndCache exec = new RequestExecutorBulkAndCache(inputBatchIterator, concurrentSlotCount, concurrentSlotReadaheadCount, execCxt, opExecutor, serviceInfo, resultSizeCache, serviceCache, cacheMode); + return new QueryIterOverAbortableIterator(execCxt, exec); } } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ChainingServiceExecutorBulkConcurrent.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ChainingServiceExecutorBulkConcurrent.java new file mode 100644 index 00000000000..48c1abbda9c --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ChainingServiceExecutorBulkConcurrent.java @@ -0,0 +1,162 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl; + +import java.util.List; +import java.util.Map.Entry; +import java.util.function.Function; + +import org.apache.jena.graph.Node; +import org.apache.jena.sparql.algebra.op.OpService; +import org.apache.jena.sparql.core.Var; +import org.apache.jena.sparql.engine.ExecutionContext; +import org.apache.jena.sparql.engine.QueryIterator; +import org.apache.jena.sparql.engine.binding.Binding; +import org.apache.jena.sparql.engine.iterator.QueryIterPlainWrapper; +import org.apache.jena.sparql.service.ServiceExec; +import org.apache.jena.sparql.service.bulk.ChainingServiceExecutorBulk; +import org.apache.jena.sparql.service.bulk.ServiceExecutorBulk; +import org.apache.jena.sparql.service.enhancer.impl.RequestExecutorBase.Granularity; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterator; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterators; +import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerConstants; +import org.apache.jena.sparql.util.Context; + +public class ChainingServiceExecutorBulkConcurrent + implements ChainingServiceExecutorBulk +{ + public static final String OPTION_NAME = "concurrent"; + + public record Config(int concurrentSlots, int bindingsPerSlot, long readAhead) {} + + private final String name; + + /** Create a service executor that performs concurrent execution, configurable via the option {@value #OPTION_NAME}. + * For example SERVICE <concurrent:> { }.*/ + public ChainingServiceExecutorBulkConcurrent() { + this(OPTION_NAME); + } + + public ChainingServiceExecutorBulkConcurrent(String optionName) { + super(); + this.name = optionName; + } + + @Override + public QueryIterator createExecution(OpService opService, QueryIterator input, ExecutionContext execCxt, ServiceExecutorBulk chain) { +// ServiceOpts opts = ServiceOpts.getEffectiveService(opService, ServiceEnhancerConstants.SELF.getURI(), +// key -> key.equals(name)); + List> list = ServiceOpts.parseEntries(opService.getService()); + + QueryIterator result; + Entry opt = list.isEmpty() ? null : list.get(0); + if (opt != null && opt.getKey().equals(name)) { + list = list.subList(1, list.size()); + // Remove a trailing colon separator + // FIXME: This should be handled more elegantly + if (!list.isEmpty() && list.get(0).getKey().equals("")) { + list = list.subList(1, list.size()); + } + + Context cxt = execCxt.getContext(); + // String key = opt.getKey(); + String val = opt.getValue(); + Config config = parseConfig(val, cxt); + + OpService newOp = ChainingServiceExecutorBulkServiceEnhancer.toOpService(list, opService, ServiceEnhancerConstants.SELF_BULK); + + // OpServiceInfo serviceInfo = new OpServiceInfo(opService); + Node serviceNode = opService.getService(); + // OpServiceInfo serviceInfo = new OpServiceInfo(newOp); + Function groupKeyFn = binding -> Var.lookup(binding, serviceNode); + // Function groupKeyFn = serviceInfo::getSubstServiceNode; + + Batcher scheduler = new Batcher<>(groupKeyFn, config.bindingsPerSlot(), 0); + AbortableIterator> inputBatchIterator = scheduler.batch(AbortableIterators.adapt(input)); + + RequestExecutorSparqlBase exec = new RequestExecutorSparqlBase(Granularity.BATCH, inputBatchIterator, config.concurrentSlots(), config.readAhead(), execCxt) { + @Override + protected AbortableIterator buildIterator(boolean runsOnNewThread, Node groupKey, List inputs, List reverseMap, ExecutionContext batchExecCxt) { +// Iterator indexedBindings = IntStream.range(0, inputs.size()).mapToObj(i -> +// BindingFactory.binding(inputs.get(i), globalIdxVar, NodeValue.makeInteger(reverseMap.get(i)).asNode())) +// .iterator(); + + QueryIterator subIter = QueryIterPlainWrapper.create(inputs.iterator(), batchExecCxt); + + // QueryIterator tmp = chain.createExecution(newOp, QueryIterPlainWrapper.create(indexedBindings, execCxt), execCxt); + // Pass the adapted request through the whole service executor chain again. + QueryIterator tmp = ServiceExec.exec(newOp, subIter, batchExecCxt); + return AbortableIterators.adapt(tmp); + } + + @Override + protected long extractInputOrdinal(Binding targetItem) { + // This iterator operates on batch granularity + // No need to relate individual bindings to their ordinal. + throw new IllegalStateException("Should never be called."); + } + }; + result = AbortableIterators.asQueryIterator(exec); + } else { + result = chain.createExecution(opService, input, execCxt); + } + return result; + } + + /** Parse the settings of format [concurrentSlots[-maxBindingsPerSlot[-maxReadaheadOfBindingsPerSlot]]]. */ + public static Config parseConfig(String val, Context cxt) { + int concurrentSlots = 0; + long readaheadOfBindingsPerSlot = ChainingServiceExecutorBulkCache.DFT_CONCURRENT_READAHEAD; + + int maxConcurrentSlotCount = cxt.get(ServiceEnhancerConstants.serviceConcurrentMaxSlotCount, ChainingServiceExecutorBulkCache.DFT_MAX_CONCURRENT_SLOTS); + + String v = val == null ? "" : val.toLowerCase().trim(); + int bindingsPerSlot = 1; + + // [{concurrentSlotCount}[-{bindingsPerSlotCount}[-{readAheadPerSlotCount}]]] + if (!v.isEmpty()) { + String[] parts = v.split("-", 3); + if (parts.length > 0) { + concurrentSlots = parseInt(parts[0], 0); + if (parts.length > 1) { + bindingsPerSlot = parseInt(parts[1], 0); + // There must be at least 1 binding per slot + bindingsPerSlot = Math.max(1, bindingsPerSlot); + if (parts.length > 2) { + int maxReadaheadOfBindingsPerSlot = cxt.get(ServiceEnhancerConstants.serviceConcurrentMaxReadaheadCount, ChainingServiceExecutorBulkCache.DFT_MAX_CONCURRENT_READAHEAD); + readaheadOfBindingsPerSlot = parseInt(parts[2], 0); + readaheadOfBindingsPerSlot = Math.max(Math.min(readaheadOfBindingsPerSlot, maxReadaheadOfBindingsPerSlot), 0); + } + } + } + } else { + concurrentSlots = Runtime.getRuntime().availableProcessors(); + } + concurrentSlots = Math.max(Math.min(concurrentSlots, maxConcurrentSlotCount), 0); + + return new Config(concurrentSlots, bindingsPerSlot, readaheadOfBindingsPerSlot); + } + + private static int parseInt(String str, int fallbackValue) { + return str.isEmpty() ? fallbackValue : Integer.parseInt(str); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ChainingServiceExecutorBulkServiceEnhancer.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ChainingServiceExecutorBulkServiceEnhancer.java index be9e3b7ac94..78be95a384a 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ChainingServiceExecutorBulkServiceEnhancer.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ChainingServiceExecutorBulkServiceEnhancer.java @@ -31,6 +31,7 @@ import org.apache.jena.sparql.algebra.op.OpService; import org.apache.jena.sparql.engine.ExecutionContext; import org.apache.jena.sparql.engine.QueryIterator; +import org.apache.jena.sparql.service.ServiceExec; import org.apache.jena.sparql.service.bulk.ChainingServiceExecutorBulk; import org.apache.jena.sparql.service.bulk.ServiceExecutorBulk; import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerConstants; @@ -43,44 +44,74 @@ public class ChainingServiceExecutorBulkServiceEnhancer public QueryIterator createExecution(OpService opService, QueryIterator input, ExecutionContext execCxt, ServiceExecutorBulk chain) { - QueryIterator result; + // Don't interfere if service node is not an IRI Node node = opService.getService(); - List> opts = ServiceOpts.parseAsOptions(node); + if (node == null || !node.isURI()) { + return chain.createExecution(opService, input, execCxt); + } + List> opts = ServiceOpts.parseEntries(node); - boolean enableBulk = false; + // The following variables will be updated based on the options + boolean useLoop = false; + boolean enableBulk = false; int bulkSize = 1; + CacheMode requestedCacheMode = null; - CacheMode cacheMode = null; Context cxt = execCxt.getContext(); int n = opts.size(); int i = 0; + + String v; outer: for (; i < n; ++i) { Entry opt = opts.get(i); String key = opt.getKey(); String val = opt.getValue(); switch (key) { - case ServiceOpts.SO_LOOP: + case ServiceOptsSE.SO_LOOP: // Loop (lateral join) is handled on the algebra level - // nothing to do here except for suppressing forward to + // nothing to do here except for suppressing its forward // to the remainder of the chain + useLoop = true; break; - case ServiceOpts.SO_CACHE: // Enables caching - String v = val == null ? "" : val.toLowerCase(); + + case ServiceOptsSE.SO_CACHE: // Enables caching + v = val == null ? "" : val.toLowerCase(); switch (v) { - case "off": cacheMode = CacheMode.OFF; break; - case "clear": cacheMode = CacheMode.CLEAR; break; - default: cacheMode = CacheMode.DEFAULT; break; + case "off": requestedCacheMode = CacheMode.OFF; break; + case "clear": requestedCacheMode = CacheMode.CLEAR; break; + default: requestedCacheMode = CacheMode.DEFAULT; break; } + break; + /* + case ServiceOptsSE.SO_CONCURRENT: + int maxConcurrentSlotCount = cxt.get(ServiceEnhancerConstants.serviceConcurrentMaxSlotCount, ChainingServiceExecutorBulkCache.DFT_MAX_CONCURRENT_SLOTS); + // Value pattern is: [concurrentSlots][-maxReadaheadOfBindingsPerSlot] + v = val == null ? "" : val.toLowerCase().trim(); + if (!v.isEmpty()) { + String[] parts = v.split("-", 2); + if (parts.length > 0) { + concurrentSlots = Integer.parseInt(parts[0]); + if (parts.length > 1) { + int maxReadaheadOfBindingsPerSlot = cxt.get(ServiceEnhancerConstants.serviceConcurrentMaxReadaheadCount, ChainingServiceExecutorBulkCache.DFT_MAX_CONCURRENT_READAHEAD); + readaheadOfBindingsPerSlot = Integer.parseInt(parts[1]); + readaheadOfBindingsPerSlot = Math.max(Math.min(readaheadOfBindingsPerSlot, maxReadaheadOfBindingsPerSlot), 0); + } + } + } else { + concurrentSlots = Runtime.getRuntime().availableProcessors(); + } + concurrentSlots = Math.max(Math.min(concurrentSlots, maxConcurrentSlotCount), 0); break; - case ServiceOpts.SO_BULK: // Enables bulk requests + */ + case ServiceOptsSE.SO_BULK: // Enables bulk requests enableBulk = true; int maxBulkSize = cxt.get(ServiceEnhancerConstants.serviceBulkMaxBindingCount, ChainingServiceExecutorBulkCache.DFT_MAX_BULK_SIZE); - bulkSize = cxt.get(ServiceEnhancerConstants.serviceBulkBindingCount, ChainingServiceExecutorBulkCache.DFT_BULK_SIZE); + bulkSize = cxt.get(ServiceEnhancerConstants.serviceBulkBindingCount, ChainingServiceExecutorBulkCache.DFT_CONCURRENT_READAHEAD); try { if (val == null || val.isBlank()) { // Ignored @@ -92,39 +123,75 @@ public QueryIterator createExecution(OpService opService, QueryIterator input, E } bulkSize = Math.max(Math.min(bulkSize, maxBulkSize), 1); break; + + case "": // Skip over separator entries + break; + default: break outer; } } List> subList = opts.subList(i, n); - String serviceStr = ServiceOpts.unparse(subList); +// String serviceStr = ServiceOpts.unparseEntries(subList); +// OpService newOp = null; +// if (serviceStr.isEmpty()) { +// Op subOp = opService.getSubOp(); +// if (subOp instanceof OpService) { +// newOp = (OpService)subOp; +// } else { +// serviceStr = ServiceEnhancerConstants.SELF.getURI(); +// } +// } +// +// if (newOp == null) { +// node = NodeFactory.createURI(serviceStr); +// newOp = new OpService(node, opService.getSubOp(), opService.getSilent()); +// } + OpService newOp = toOpService(subList, opService, ServiceEnhancerConstants.SELF); + + QueryIterator result; + CacheMode finalCacheMode = CacheMode.effectiveMode(requestedCacheMode); + + int concurrentSlots = 0; // FIXME Remove because concurrent is always disabled now; was factored out from here + long readaheadOfBindingsPerSlot = ChainingServiceExecutorBulkCache.DFT_CONCURRENT_READAHEAD; + + boolean enableConcurrent = concurrentSlots > 0; + boolean applySpecialProcessing = + finalCacheMode != CacheMode.OFF || + enableBulk || + enableConcurrent; + + if (applySpecialProcessing) { + ChainingServiceExecutorBulkCache exec = new ChainingServiceExecutorBulkCache(bulkSize, finalCacheMode, concurrentSlots, readaheadOfBindingsPerSlot); + result = exec.createExecution(newOp, input, execCxt, ServiceExec::exec); + } else if (useLoop) { + // We don't need special bulk/cache processing, but we removed loop from the serviceIRI + // So restart the chain + result = ServiceExec.exec(newOp, input, execCxt); + } else { + result = chain.createExecution(newOp, input, execCxt); + } + return result; + } + + public static OpService toOpService(List> list, OpService originalOpService, Node fallbackServiceIri) { + String serviceStr = ServiceOpts.unparseEntries(list); OpService newOp = null; if (serviceStr.isEmpty()) { - Op subOp = opService.getSubOp(); - if (subOp instanceof OpService) { - newOp = (OpService)subOp; + Op subOp = originalOpService.getSubOp(); + if (subOp instanceof OpService subService) { + newOp = subService; } else { - serviceStr = ServiceEnhancerConstants.SELF.getURI(); + serviceStr = fallbackServiceIri.getURI(); // ServiceEnhancerConstants.SELF.getURI(); } } if (newOp == null) { - node = NodeFactory.createURI(serviceStr); - newOp = new OpService(node, opService.getSubOp(), opService.getSilent()); + Node node = NodeFactory.createURI(serviceStr); + newOp = new OpService(node, originalOpService.getSubOp(), originalOpService.getSilent()); } - CacheMode effCacheMode = CacheMode.effectiveMode(cacheMode); - - boolean enableSpecial = effCacheMode != CacheMode.OFF || enableBulk; // || enableLoopJoin; // || !overrides.isEmpty(); - - if (enableSpecial) { - ChainingServiceExecutorBulkCache exec = new ChainingServiceExecutorBulkCache(bulkSize, effCacheMode); - result = exec.createExecution(newOp, input, execCxt, chain); - } else { - result = chain.createExecution(newOp, input, execCxt); - } - - return result; + return newOp; } } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ExecutorServiceWrapperSync.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ExecutorServiceWrapperSync.java new file mode 100644 index 00000000000..30e97ef27fe --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ExecutorServiceWrapperSync.java @@ -0,0 +1,73 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** Utility wrapper for an ExecutorService to add synchronous API that abstracts away the Future. */ +public class ExecutorServiceWrapperSync { + protected ExecutorService executorService; + + public ExecutorServiceWrapperSync() { + this(null); + } + + public ExecutorServiceWrapperSync(ExecutorService es) { + super(); + this.executorService = es; + } + + public ExecutorService getExecutorService() { + return executorService; + } + + public void submit(Runnable runnable) { + submit(() -> { runnable.run(); return null; }); + } + + public T submit(Callable callable) { + if (executorService == null) { + synchronized (this) { + if (executorService == null) { + executorService = Executors.newSingleThreadExecutor(); + } + } + } + T result = submit(executorService, callable); + return result; + } + + /** Execute the callable on the executor service and return its result. */ + public static T submit(ExecutorService executorService, Callable callable) { + try { + Future future = executorService.submit(callable); + T result = future.get(); + return result; + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/GroupedBatch.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/GroupedBatch.java index 1b4c457ce08..1e56a8219b9 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/GroupedBatch.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/GroupedBatch.java @@ -21,8 +21,30 @@ package org.apache.jena.sparql.service.enhancer.impl; -/** Interface that combines a group key with a {@link Batch} */ -public interface GroupedBatch, V> { - G getGroupKey(); - Batch getBatch(); -} \ No newline at end of file +/** + * Implementation that combines a batch with a group key. + */ +public class GroupedBatch, V> +{ + protected G groupKey; + protected Batch batch; + + public GroupedBatch(G groupKey, Batch batch) { + super(); + this.groupKey = groupKey; + this.batch = batch; + } + + public G getGroupKey() { + return groupKey; + } + + public Batch getBatch() { + return batch; + } + + @Override + public String toString() { + return "GroupedBatch [groupKey=" + groupKey + ", batch=" + batch + "]"; + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/IteratorFactoryWithBuffer.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/IteratorFactoryWithBuffer.java index 827512224e5..c448dbd661a 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/IteratorFactoryWithBuffer.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/IteratorFactoryWithBuffer.java @@ -27,12 +27,13 @@ import java.util.SortedSet; import java.util.stream.IntStream; +import org.apache.jena.query.QueryCancelledException; +import org.apache.jena.sparql.service.enhancer.impl.util.SinglePrefetchIterator; + import com.google.common.collect.MultimapBuilder; import com.google.common.collect.PeekingIterator; import com.google.common.collect.SetMultimap; -import org.apache.jena.sparql.service.enhancer.impl.util.SinglePrefetchIterator; - /** * Buffering iterator. Can buffer an arbitrary amount ahead. * @@ -223,6 +224,16 @@ protected T prefetch() { return result; } + @Override + protected void handleException(Throwable e) { + if (e instanceof QueryCancelledException) { + e.addSuppressed(new RuntimeException("Prefetching data failed.")); + throw (QueryCancelledException)e; + } + super.handleException(e); + } + + /** Close only removes this sub-iterator's position from the 'offsetToChild' map. */ @Override public void close() { synchronized (lock) { @@ -293,4 +304,4 @@ public static void main(String[] args) { } } -} \ No newline at end of file +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/Managed.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/Managed.java new file mode 100644 index 00000000000..fe495658e85 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/Managed.java @@ -0,0 +1,93 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl; + +import java.util.Objects; +import java.util.function.Consumer; + +import org.apache.jena.shared.ClosedException; + +public class Managed { + private T resource; + private Consumer closer; + + private Object lock = new Object(); + private volatile boolean isClosed = false; + + public Managed(Consumer closer) { + super(); + this.closer = closer; + } + + public static Managed of(Consumer closer) { + Objects.requireNonNull(closer); + return new Managed<>(closer); + } + + public T get() { + checkOpen(); + return resource; + } + + protected void checkOpen() { + if (isClosed) { + throw new ClosedException(null, null); + } + } + + public void set(T newResource) { + checkOpen(); + synchronized (lock) { + checkOpen(); + if (resource != newResource) { + if (resource != null) { + try { + closer.accept(resource); + } finally { + resource = newResource; + } + } else { + resource = newResource; + } + } + } + } + + public void close() { + if (!isClosed) { + synchronized (lock) { + if (!isClosed) { + try { + if (resource != null) { + closer.accept(resource); + } + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + isClosed = true; + resource = null; + } + } + } + } + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/Meter.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/Meter.java new file mode 100644 index 00000000000..674c002d874 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/Meter.java @@ -0,0 +1,62 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl; + +import java.util.Deque; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.atomic.AtomicLong; + +public class Meter { + private Deque> data; + private long total = 0; + private int maxDataSize; + + private long lastTick = -1; + private AtomicLong counter = new AtomicLong(); + + public Meter(int maxDataSize) { + super(); + if (maxDataSize < 1) { + throw new IllegalArgumentException("Data size must be at least 1."); + } + this.maxDataSize = maxDataSize; + } + + public void inc() { + counter.incrementAndGet(); + } + + public void tick() { + long time = System.currentTimeMillis(); + long value = counter.getAndSet(0); + + if (data.size() >= maxDataSize) { + Entry e = data.removeFirst(); + total -= e.getValue(); + } + + total += value; + data.add(Map.entry(time, value)); + lastTick = time; + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/OpServiceExecutor.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/OpServiceExecutor.java index 7eb1eaeb10f..bb2f5d24589 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/OpServiceExecutor.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/OpServiceExecutor.java @@ -22,10 +22,11 @@ package org.apache.jena.sparql.service.enhancer.impl; import org.apache.jena.sparql.algebra.op.OpService; +import org.apache.jena.sparql.engine.ExecutionContext; import org.apache.jena.sparql.engine.QueryIterator; /** Interface for directly executing {@link OpService} instances */ @FunctionalInterface public interface OpServiceExecutor { - QueryIterator exec(OpService opService); + QueryIterator exec(OpService opService, ExecutionContext execCxt); } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/OpServiceExecutorImpl.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/OpServiceExecutorImpl.java index 92fc057881f..e90638f35d8 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/OpServiceExecutorImpl.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/OpServiceExecutorImpl.java @@ -38,27 +38,21 @@ public class OpServiceExecutorImpl implements OpServiceExecutor { protected OpService originalOp; - protected ExecutionContext execCxt; protected ServiceExecutorBulk delegate; - public OpServiceExecutorImpl(OpService opService, ExecutionContext execCxt, ServiceExecutorBulk delegate) { + public OpServiceExecutorImpl(OpService opService, ServiceExecutorBulk delegate) { this.originalOp = opService; - this.execCxt = execCxt; this.delegate = delegate; } - public ExecutionContext getExecCxt() { - return execCxt; - } - @Override - public QueryIterator exec(OpService substitutedOp) { + public QueryIterator exec(OpService substitutedOp, ExecutionContext execCxt) { QueryIterator result; Binding input = BindingFactory.binding(); boolean silent = originalOp.getSilent(); try { - QueryIterator singleton = QueryIterSingleton.create(BindingFactory.root(), execCxt); + QueryIter singleton = QueryIterSingleton.create(BindingFactory.root(), execCxt); result = delegate.createExecution(substitutedOp, singleton, execCxt); // ---- Execute diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/OpServiceInfo.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/OpServiceInfo.java index e9cd9d51523..ee97337b322 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/OpServiceInfo.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/OpServiceInfo.java @@ -25,7 +25,6 @@ import java.util.Map; import java.util.Set; -import com.google.common.collect.BiMap; import org.apache.jena.graph.Node; import org.apache.jena.query.Query; import org.apache.jena.sparql.algebra.Op; @@ -41,6 +40,8 @@ import org.apache.jena.sparql.service.enhancer.impl.util.VarScopeUtils; import org.apache.jena.sparql.syntax.syntaxtransform.NodeTransformSubst; +import com.google.common.collect.BiMap; + /** * Class used to map a given scoped OpService to a normalized form. Several methods abbreviate * normalized with normed. @@ -88,15 +89,18 @@ public OpServiceInfo(OpService opService) { this.offset = Query.NOLIMIT; } + Set visibleSubOpVars = OpVars.visibleVars(baseSubOp); Collection mentionedSubOpVars = OpVars.mentionedVars(baseSubOp); + + Set visiblePlainNames = VarScopeUtils.getPlainNames(visibleSubOpVars); + // mentionedSubOpVarsScopedToNormed = VarUtils.normalizeVarScopesGlobal(mentionedSubOpVars); - mentionedSubOpVarsScopedToNormed = VarScopeUtils.normalizeVarScopes(mentionedSubOpVars); + mentionedSubOpVarsScopedToNormed = VarScopeUtils.normalizeVarScopes(mentionedSubOpVars, visiblePlainNames); normedQueryOp = NodeTransformLib.transform(new NodeTransformSubst(mentionedSubOpVarsScopedToNormed), baseSubOp); // Handling of a null supOp - can that happen? - Set visibleSubOpVars = OpVars.visibleVars(baseSubOp); - this.visibleSubOpVarsScopedToNorm = VarScopeUtils.normalizeVarScopes(visibleSubOpVars); + this.visibleSubOpVarsScopedToNorm = VarScopeUtils.normalizeVarScopes(visibleSubOpVars, visiblePlainNames); this.normedQuery = OpAsQuery.asQuery(normedQueryOp); diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/PartitionRequest.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/PartitionRequest.java index 63ff6d86c28..9a7db5dc436 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/PartitionRequest.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/PartitionRequest.java @@ -22,45 +22,12 @@ package org.apache.jena.sparql.service.enhancer.impl; /** - * Helper class to capture a range of data (specified by limit + offset) + * Helper record to capture a range of data (specified by limit + offset) * w.r.t. a partition key (typically a {@link org.apache.jena.sparql.engine.binding.Binding} * and give that information an id. */ -public class PartitionRequest +public record PartitionRequest(long outputId, I partitionKey, long offset, long limit) { - protected long outputId; - protected I partitionKey; - protected long offset; - protected long limit; - - public PartitionRequest( - long outputId, - I partition, - long offset, - long limit) { - super(); - this.outputId = outputId; - this.partitionKey = partition; - this.offset = offset; - this.limit = limit; - } - - public long getOutputId() { - return outputId; - } - - public I getPartitionKey() { - return partitionKey; - } - - public long getOffset() { - return offset; - } - - public long getLimit() { - return limit; - } - public boolean hasOffset() { return offset > 0; } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/PrefetchTaskBase.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/PrefetchTaskBase.java new file mode 100644 index 00000000000..f6968d03a5f --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/PrefetchTaskBase.java @@ -0,0 +1,132 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.function.UnaryOperator; + +/** + * A task that buffers items from an iterator in a list upon calling {@link #run()}. + * The task runs until {@link #stop()} is called or the thread is interrupted. + */ +public class PrefetchTaskBase> + implements Runnable +{ + public enum State { + CREATED, + STARTING, + RUNNING, + TERMINATED + } + + protected volatile X iterator; + protected UnaryOperator itemCopyFn; + protected volatile List bufferedItems; + protected long maxBufferedItemsCount; + + protected volatile boolean isStopRequested; + + protected volatile State state = State.CREATED; + + /** When a tasks terminates in failure then this field is set. + * Consequently, {@link #run()} will never fail with an exception. */ + protected volatile Throwable throwable = null; + + public PrefetchTaskBase(X iterator, long maxBufferedItemsCount, UnaryOperator copyFn) { + this(iterator, new ArrayList<>(1024), maxBufferedItemsCount, copyFn); + } + + /** + * + * @param iterator + * @param bufferedItems + * @param maxBufferedItemsCount + * @param copyFn A function to copy items before buffering them. + * Can be used to detach items from resources. + * The copyFn be null. + */ + public PrefetchTaskBase(X iterator, List bufferedItems, long maxBufferedItemsCount, UnaryOperator copyFn) { + super(); + this.maxBufferedItemsCount = maxBufferedItemsCount; + this.iterator = iterator; + this.bufferedItems = bufferedItems; + this.itemCopyFn = copyFn; + } + + public List getBufferedItems() { + return bufferedItems; + } + + public X getIterator() { + return iterator; + } + + public State getState() { + return state; + } + + public UnaryOperator getCopyFn() { + return itemCopyFn; + } + + @Override + public final void run() { + // Before the first item has been processed the state remains in STARTING. + state = State.STARTING; + try { + runActual(); + } catch (Throwable t) { + this.throwable = t; + } finally { + try { + afterRun(); + } finally { + state = State.TERMINATED; + } + } + } + + protected void afterRun() {} + + protected void runActual() { + state = State.RUNNING; + while (!isStopRequested && !Thread.interrupted() && iterator.hasNext() && bufferedItems.size() < maxBufferedItemsCount) { + T item = iterator.next(); + T copy = itemCopyFn == null ? item : itemCopyFn.apply(item); + bufferedItems.add(copy); + } + } + + public Throwable getThrowable() { + return throwable; + } + + public void stop() { + isStopRequested = true; + } + + public static > PrefetchTaskBase of(I iterator, long maxBufferedItemsCount, UnaryOperator copyFn) { + return new PrefetchTaskBase<>(iterator, maxBufferedItemsCount, copyFn); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/QueryIterServiceBulk.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/QueryIterServiceBulkAndCache.java similarity index 84% rename from jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/QueryIterServiceBulk.java rename to jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/QueryIterServiceBulkAndCache.java index d47ac87b203..a9d067d54e9 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/QueryIterServiceBulk.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/QueryIterServiceBulkAndCache.java @@ -36,14 +36,6 @@ import org.apache.jena.atlas.iterator.IteratorCloseable; import org.apache.jena.atlas.iterator.IteratorOnClose; import org.apache.jena.atlas.lib.Closeable; -import com.google.common.collect.Iterators; -import com.google.common.collect.Range; -import com.google.common.collect.RangeMap; -import com.google.common.collect.RangeSet; -import com.google.common.collect.TreeBasedTable; -import com.google.common.collect.TreeRangeMap; -import com.google.common.collect.TreeRangeSet; -import com.google.common.math.LongMath; import org.apache.jena.graph.Node; import org.apache.jena.query.Query; import org.apache.jena.sparql.algebra.Algebra; @@ -66,6 +58,8 @@ import org.apache.jena.sparql.service.enhancer.impl.util.BindingUtils; import org.apache.jena.sparql.service.enhancer.impl.util.QueryIterDefer; import org.apache.jena.sparql.service.enhancer.impl.util.QueryIterSlottedBase; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterators; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.QueryIteratorOverAbortableIterator; import org.apache.jena.sparql.service.enhancer.slice.api.IteratorOverReadableChannel; import org.apache.jena.sparql.service.enhancer.slice.api.ReadableChannel; import org.apache.jena.sparql.service.enhancer.slice.api.ReadableChannelOverSliceAccessor; @@ -76,16 +70,23 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.collect.Iterators; +import com.google.common.collect.Range; +import com.google.common.collect.RangeMap; +import com.google.common.collect.RangeSet; +import com.google.common.collect.TreeBasedTable; +import com.google.common.collect.TreeRangeMap; +import com.google.common.collect.TreeRangeSet; +import com.google.common.math.LongMath; + /** * QueryIter to process service requests in bulk with support for streaming caching. - * - * The methods closeIterator and moveToNext are synchronized. - * */ -public class QueryIterServiceBulk +public class QueryIterServiceBulkAndCache extends QueryIterSlottedBase + // extends AbstractAbortableIterator { - private static final Logger logger = LoggerFactory.getLogger(QueryIterServiceBulk.class); + private static final Logger logger = LoggerFactory.getLogger(QueryIterServiceBulkAndCache.class); protected OpServiceInfo serviceInfo; protected ServiceCacheKeyFactory cacheKeyFactory; @@ -138,7 +139,7 @@ public class QueryIterServiceBulk // Close a sliceKey's iterator upon exhaustion if they slice key is in the set protected Set sliceKeyToClose = new HashSet<>(); - public QueryIterServiceBulk( + public QueryIterServiceBulkAndCache( OpServiceInfo serviceInfo, BatchQueryRewriter batchQueryRewriter, ServiceCacheKeyFactory cacheKeyFactory, @@ -149,6 +150,8 @@ public QueryIterServiceBulk( ServiceResponseCache cache, CacheMode cacheMode ) { + super(execCxt); + this.serviceInfo = serviceInfo; this.cacheKeyFactory = cacheKeyFactory; this.opExecutor = opExecutor; @@ -176,8 +179,21 @@ protected void advanceInput(boolean resetRangeId) { } } +// @Override +// protected Binding moveToNext() { +// Binding result; +// try { +// result = moveToNextActual(); +// } catch (Exception e) { +// freeResources(); +// e.addSuppressed(new RuntimeException("Problem encountered moving to next item.")); +// throw e; +// } +// return result; +// } + @Override - protected synchronized Binding moveToNext() { + protected Binding moveToNext() { Binding mergedBindingWithIdx = null; // One time init @@ -250,7 +266,6 @@ protected synchronized Binding moveToNext() { // If there is insufficient buffer available we can still try whether we see a result set limit // alternatively we could just set resetRequest to true - boolean isResultSetLimitReached = false; // reached end without seeing the end marker while (obtainedRowCount < remainingNeededBackendRowCount) { // Repeat until we can serve another binding @@ -337,34 +352,36 @@ protected synchronized Binding moveToNext() { if (activeIt != null) { if (activeIt.hasNext()) { + // Peek the next binding from the active iterator. Binding peek = activeIt.peek(); int peekOutputId = BindingUtils.getNumber(peek, idxVar).intValue(); if (BatchQueryRewriter.isRemoteEndMarker(peekOutputId)) { - // Attempt to move to the next range - ++currentRangeId; - continue; - } - - SliceKey sliceKey = outputToSliceKey.get(peekOutputId); - - if (sliceKey == null) { - throw new IllegalStateException( - String.format("An output binding referred to an input id without corresponding input binding. Referenced input id %1$d, Output binding: %2$s", peekOutputId, peek)); - } + // If that binding was the end marker just fall through - this + // moves the active iterator to the next range or input and + // retries the loop. + } else { + // Process the peeked binding into a result binding. + SliceKey sliceKey = outputToSliceKey.get(peekOutputId); + + if (sliceKey == null) { + throw new IllegalStateException( + String.format("An output binding referred to an input id without corresponding input binding. Referenced input id %1$d, Output binding: %2$s", peekOutputId, peek)); + } - boolean matchesCurrentPartition = sliceKey.getInputId() == currentInputId && - sliceKey.getRangeId() == currentRangeId; + boolean matchesCurrentPartition = sliceKey.getInputId() == currentInputId && + sliceKey.getRangeId() == currentRangeId; - if (matchesCurrentPartition) { - Binding parentBinding = inputs.get(currentInputId); - Binding childBindingWithIdx = activeIt.next(); + if (matchesCurrentPartition) { + Binding parentBinding = inputs.get(currentInputId); + Binding childBindingWithIdx = activeIt.next(); - // Check for compatibility - mergedBindingWithIdx = Algebra.merge(parentBinding, childBindingWithIdx); - if (mergedBindingWithIdx == null) { - continue; - } else { - break; + // Check for compatibility + mergedBindingWithIdx = Algebra.merge(parentBinding, childBindingWithIdx); + if (mergedBindingWithIdx == null) { + continue; + } else { + break; + } } } } else { @@ -381,7 +398,7 @@ protected synchronized Binding moveToNext() { SliceKey sliceKey = new SliceKey(currentInputId, currentRangeId); if (sliceKeyToClose.contains(sliceKey)) { - // System.out.println("Closing part key " + pk); + // System.out.println("Closing slice key " + pk); Closeable closeable = sliceKeyToIter.get(sliceKey); closeable.close(); sliceKeyToClose.remove(sliceKey); @@ -416,6 +433,7 @@ protected synchronized Binding moveToNext() { } if (result == null) { + result = endOfData(); freeResources(); } @@ -434,6 +452,7 @@ public SliceKey getPartKeyFromBinding(Binding binding) { protected void freeResources() { if (backendIt != null) { + backendIt.getDelegate().close(); backendIt.close(); } @@ -441,6 +460,7 @@ protected void freeResources() { Closeable closeable = sliceKeyToIter.get(partKey); closeable.close(); } + sliceKeyToClose.clear(); inputToRangeToOutput.clear(); @@ -452,16 +472,23 @@ protected void freeResources() { } @Override - public synchronized void closeIterator() { + public void closeIteratorActual() { freeResources(); } - /** Prepare the lazy execution of the next batch and register all iterators with {@link #sliceKeyToIter} */ + @Override + protected void requestCancel() { + } + + /** + * Prepare the lazy execution of the next batch and register all iterators with {@link #sliceKeyToIter}. + * Only called from {@link #moveToNext()}. + */ // seqId = sequential number injected into the request // inputId = id (index) of the input binding // rangeId = id of the range w.r.t. to the input binding // sliceKey = (inputId, rangeId) - public void prepareNextBatchExec(boolean bypassCacheOnFirstInput) { + protected void prepareNextBatchExec(boolean bypassCacheOnFirstInput) { freeResources(); @@ -494,18 +521,22 @@ public void prepareNextBatchExec(boolean bypassCacheOnFirstInput) { // Binding joinBinding = new BindingProject(joinVarMap.keySet(), inputBinding); Slice slice = null; - Lock lock = null; + Lock sliceReadLock = null; RefFuture cacheValueRef = null; if (cache != null) { - ServiceCacheKey cacheKey = cacheKeyFactory.createCacheKey(inputBinding); - // ServiceCacheKey cacheKey = new ServiceCacheKey(targetService, serviceInfo.getRawQueryOp(), joinBinding, useLoopJoin); - // System.out.println("Lookup with cache key " + cacheKey); + if (logger.isDebugEnabled()) { + logger.debug("Created cache key: {}", cacheKey); + } // Note: cacheValueRef must be closed as part of the iterators that read from the cache cacheValueRef = cache.getCache().claim(cacheKey); + ServiceCacheValue serviceCacheValue = cacheValueRef.await(); + if (logger.isDebugEnabled()) { + logger.debug("Claimed slice for key {} with state {}.", cacheKey, serviceCacheValue); + } // Lock an existing cache entry so we can read out the loaded ranges slice = serviceCacheValue.getSlice(); @@ -514,14 +545,14 @@ public void prepareNextBatchExec(boolean bypassCacheOnFirstInput) { slice.clear(); } - lock = slice.getReadWriteLock().readLock(); - - if (logger.isDebugEnabled()) { - logger.debug("Created cache key: " + cacheKey); - } + // Locking for reading prevents any further changes to the slice's metadata, such as loaded ranges. + sliceReadLock = slice.getReadWriteLock().readLock(); // Log.debug(BatchRequestIterator.class, "Cached ranges: " + slice.getLoadedRanges().toString()); - lock.lock(); + // FIXME I think locking the slice must immediately add an eviction guard for all data in the slice. + // FIXME Right now its done in a separate step which I think can cause a race condition!!! + // FIXME Also, if we know the cache size here, then we can cleverly stop caching batches that are outside of the max cache size! -so this way, rerunning the same query again will use the cache. + sliceReadLock.lock(); } RangeSet loadedRanges; @@ -566,6 +597,7 @@ public void prepareNextBatchExec(boolean bypassCacheOnFirstInput) { ? Range.atLeast(start) : Range.closedOpen(start, end); + Range lastPresentRange = null; RangeMap allRanges = TreeRangeMap.create(); if (bypassCacheOnFirstInput && isFirstInput) { allRanges.put(requestedRange, false); @@ -573,10 +605,14 @@ public void prepareNextBatchExec(boolean bypassCacheOnFirstInput) { // based on 'currentInputIdBindingsServed' } else { RangeSet presentRanges = loadedRanges.subRangeSet(requestedRange); - RangeSet absentRanges = loadedRanges.complement().subRangeSet(requestedRange); + RangeSet absentRanges = loadedRanges.complement().subRangeSet(requestedRange); // FIXME This is RangeUtils.gaps?! presentRanges.asRanges().forEach(r -> allRanges.put(r, true)); absentRanges.asRanges().forEach(r -> allRanges.put(r, false)); + + if (!presentRanges.isEmpty()) { + lastPresentRange = presentRanges.asDescendingSetOfRanges().iterator().next(); + } } // If the beginning of the request range is covered by a cache then serve from it @@ -621,21 +657,24 @@ public void prepareNextBatchExec(boolean bypassCacheOnFirstInput) { boolean usesCacheRead = false; while (rangeIt.hasNext()) { SliceKey sliceKey = new SliceKey(inputId, rangeId); - Entry, Boolean> f = rangeIt.next(); + Entry, Boolean> rangeAndState = rangeIt.next(); - Range range = f.getKey(); - boolean isLoaded = f.getValue(); + Range range = rangeAndState.getKey(); + boolean isLoaded = rangeAndState.getValue(); long lo = range.lowerEndpoint(); long hi = range.hasUpperBound() ? range.upperEndpoint() : Long.MAX_VALUE; long lim = hi == Long.MAX_VALUE ? Long.MAX_VALUE : hi - lo; - if (isLoaded) { + if (isLoaded) { // Implies (slice != null). usesCacheRead = true; + // Set up a an accessor for serving the cached data from the slice. + // Make sure to protect the cached data from eviction. + // Accessor will be closed via channel below SliceAccessor accessor = slice.newSliceAccessor(); - // Prevent eviction of the scheduled range + // Prevent eviction of the scheduled range. accessor.addEvictionGuard(Range.closedOpen(lo, hi)); // Create a channel over the accessor for sequential reading @@ -651,15 +690,14 @@ public void prepareNextBatchExec(boolean bypassCacheOnFirstInput) { IteratorCloseable baseIt = new IteratorOverReadableChannel<>(channel.getArrayOps(), channel, 1024 * 4); // The last iterator's close method also unclaims the cache entry - Runnable cacheEntryCloseAction = rangeIt.hasNext() || finalCacheValueRef == null - ? baseIt::close - : () -> { - baseIt.close(); - finalCacheValueRef.close(); - }; + IteratorCloseable coreIt = !range.equals(lastPresentRange) || finalCacheValueRef == null + ? baseIt + : Iter.onClose(baseIt, () -> { + finalCacheValueRef.close(); + }); // Bridge the cache iterator to jena - QueryIterator qIterA = QueryIterPlainWrapper.create(Iter.onClose(baseIt, cacheEntryCloseAction), execCxt); + QueryIterator qIterA = QueryIterPlainWrapper.create(coreIt, execCxt); Map normedToScoped = serviceInfo.getVisibleSubOpVarsNormedToScoped(); qIterA = new QueryIteratorMapped(qIterA, normedToScoped); @@ -670,10 +708,9 @@ public void prepareNextBatchExec(boolean bypassCacheOnFirstInput) { BindingFactory.binding(b, idxVar, NodeFactoryExtra.intToNode(idxVarValue)), execCxt); QueryIterPeek it = QueryIterPeek.create(qIterB, execCxt); - sliceKeyToIter.put(sliceKey, it); sliceKeyToClose.add(sliceKey); - } else { + } else { // if range is not loaded into cache then schedule a backend request PartitionRequest request = new PartitionRequest<>(nextAllocOutputId, inputBinding, lo, lim); backendRequests.put(nextAllocOutputId, request); sliceKeysForBackend.add(sliceKey); @@ -692,16 +729,16 @@ public void prepareNextBatchExec(boolean bypassCacheOnFirstInput) { } } } finally { - if (lock != null) { - lock.unlock(); + if (sliceReadLock != null) { + sliceReadLock.unlock(); } } rangeId = 0; } - // Create *deferred* a remote execution if needed - // A limit on the query may cause the deferred execution to never run + // Create a *deferred* backend query execution if needed. + // A limit on the query may cause the deferred execution to never run. if (!backendRequests.isEmpty()) { BatchQueryRewriteResult rewrite = batchQueryRewriter.rewrite(backendRequests); // System.out.println(rewrite); @@ -713,15 +750,17 @@ public void prepareNextBatchExec(boolean bypassCacheOnFirstInput) { // (1) we can merge it with other backend and cache requests in the right order // (2) responses are written to the cache Supplier qIterSupplier = () -> { - QueryIterator r = opExecutor.exec(substitutedOp); + QueryIterator r = opExecutor.exec(substitutedOp, execCxt); return r; }; - QueryIterator qIter = new QueryIterDefer(qIterSupplier); + QueryIterator qIter = new QueryIterDefer(execCxt, qIterSupplier); + qIter = new QueryIteratorOverAbortableIterator(AbortableIterators.adapt(qIter)); // Wrap the iterator such that the items are cached if (cache != null) { - qIter = new QueryIterWrapperCache(qIter, cacheBulkSize, cache, cacheKeyFactory, backendRequests, idxVar, targetService); + qIter = AbortableIterators.asQueryIterator( + new QueryIterWrapperCache(execCxt, qIter, cacheBulkSize, cache, cacheKeyFactory, backendRequests, idxVar, targetService)); } // Apply renaming after cache to avoid mismatch between op and bindings @@ -770,4 +809,3 @@ protected SliceKey getSliceKeyForOutputId(int outputId) { return outputToSliceKey.get(outputId); } } - diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/QueryIterWrapperCache.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/QueryIterWrapperCache.java index f062fa415fe..d987c04dcbf 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/QueryIterWrapperCache.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/QueryIterWrapperCache.java @@ -28,25 +28,30 @@ import java.util.List; import java.util.NavigableMap; +import org.apache.jena.atlas.io.IndentedWriter; import org.apache.jena.atlas.logging.Log; -import com.google.common.collect.AbstractIterator; -import com.google.common.collect.Iterators; -import com.google.common.collect.Table.Cell; -import com.google.common.math.LongMath; import org.apache.jena.graph.Node; import org.apache.jena.sparql.core.Var; +import org.apache.jena.sparql.engine.ExecutionContext; import org.apache.jena.sparql.engine.QueryIterator; import org.apache.jena.sparql.engine.binding.Binding; import org.apache.jena.sparql.engine.binding.BindingFactory; +import org.apache.jena.sparql.serializer.SerializationContext; import org.apache.jena.sparql.service.enhancer.claimingcache.RefFuture; +import org.apache.jena.sparql.service.enhancer.concurrent.AutoLock; import org.apache.jena.sparql.service.enhancer.impl.util.BindingUtils; import org.apache.jena.sparql.service.enhancer.impl.util.IteratorUtils; -import org.apache.jena.sparql.service.enhancer.impl.util.QueryIterSlottedBase; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbstractAbortableIterator; import org.apache.jena.sparql.service.enhancer.slice.api.Slice; import org.apache.jena.sparql.service.enhancer.slice.api.SliceAccessor; +import com.google.common.collect.AbstractIterator; +import com.google.common.collect.Iterators; +import com.google.common.collect.Table.Cell; +import com.google.common.math.LongMath; + public class QueryIterWrapperCache - extends QueryIterSlottedBase + extends AbstractAbortableIterator { protected AbstractIterator>> mergeLeftJoin; @@ -75,6 +80,7 @@ public class QueryIterWrapperCache protected AbstractIterator batchOutputIdIt; public QueryIterWrapperCache( + ExecutionContext execCxt, QueryIterator qIter, int batchSize, ServiceResponseCache cache, @@ -85,6 +91,9 @@ public QueryIterWrapperCache( Var idxVar, Node serviceNode ) { + //super(qIter, execCxt); + super(); + // this.execCxt = execCxt; this.inputIter = qIter; this.batchSize = batchSize; this.cache = cache; @@ -93,10 +102,28 @@ public QueryIterWrapperCache( this.idxVar = idxVar; this.serviceNode = serviceNode; this.currentBatchIt = null; + + + // ArrayList debug = new ArrayList<>(inputBatch.getItems().keySet()); + // if (debug.size() == 1 && debug.get(0) == 0) { + // System.err.println("debug point " + debug); + // } + + // XXX Push abort down to the iterators of the join? Presently, abort is handled on this QueryIter. + /* + mergeLeftJoin = IteratorUtils.partialLeftMergeJoin( + AbortableIterators.concat( + AbortableIterators.wrap(inputBatch.getItems().keySet()), + AbortableIterators.wrap(Arrays.asList(BatchQueryRewriter.REMOTE_END_MARKER))), + AbortableIterators.adapt(qIter), + outputId -> outputId, + binding -> BindingUtils.getNumber(binding, idxVar).intValue() + ); + */ mergeLeftJoin = IteratorUtils.partialLeftMergeJoin( Iterators.concat( - inputBatch.getItems().keySet().iterator(), - Arrays.asList(BatchQueryRewriter.REMOTE_END_MARKER).iterator()), + inputBatch.getItems().keySet().iterator(), + Arrays.asList(BatchQueryRewriter.REMOTE_END_MARKER).iterator()), qIter, outputId -> outputId, binding -> BindingUtils.getNumber(binding, idxVar).intValue() @@ -120,7 +147,7 @@ protected Binding moveToNext() { if (!currentBatchIt.hasNext()) { closeCurrentCacheResources(); - result = null; + result = endOfData(); break; } } @@ -140,7 +167,7 @@ protected void setupForNextLhsBinding() { if (!BatchQueryRewriter.isRemoteEndMarker(outputId)) { inputPart = inputs.get(outputId); - Binding inputBinding = inputPart.getPartitionKey(); + Binding inputBinding = inputPart.partitionKey(); // System.out.println("Moving to inputBinding " + inputBinding); ServiceCacheKey cacheKey = cacheKeyFactory.createCacheKey(inputBinding); @@ -200,10 +227,11 @@ public void prepareNextBatch() { Binding rawOutputBinding = rhs.next(); clientBatch.add(rawOutputBinding); - // Cut away the idx value for the binding in the cache + // Cut away the idx value for the binding in the cache. Binding outputBinding = BindingUtils.project(rawOutputBinding, rawOutputBinding.vars(), idxVar); arr[arrLen++] = outputBinding; } + // Update the following stats only once after the loop. remainingBatchCapacity -= arrLen; processedBindingCount += arrLen; } @@ -211,19 +239,21 @@ public void prepareNextBatch() { boolean isRhsExhausted = rhs == null || !rhs.hasNext(); // Submit batch so far - long inputOffset = inputPart.getOffset(); - long inputLimit = inputPart.getLimit(); + long inputOffset = inputPart.offset(); + long inputLimit = inputPart.limit(); long start = inputOffset + currentOffset; long end = start + arrLen; currentOffset += arrLen; cacheDataAccessor.claimByOffsetRange(start, end); - cacheDataAccessor.lock(); - try { + Slice slice = cacheDataAccessor.getSlice(); + + // cacheDataAccessor.lock(); + // Lock the whole slice to update data and metadata both atomically. + try (AutoLock sliceWriteLock = AutoLock.lock(slice.getReadWriteLock().writeLock())) { cacheDataAccessor.write(start, arr, 0, arrLen); - Slice slice = cacheDataAccessor.getSlice(); // If rhs is completely empty (without any data) then only update the slice metadata if (isRhsExhausted) { @@ -256,28 +286,29 @@ public void prepareNextBatch() { if (isKeyCompleted) { if (isEndKnown) { if (currentOffset > 0) { - slice.mutateMetaData(metaData -> metaData.setKnownSize(end)); + slice.setKnownSize(end); } else { // If we saw no binding we don't know at which point the data actually ended // but the start(=end) point is an upper limit // Note: Setting the maximum size to zero will make it a known size of 0 - slice.mutateMetaData(metaData -> metaData.setMaximumKnownSize(end)); + slice.setMaximumKnownSize(end); } } else { // Data retrieval ended at a limit (e.g. we retrieved 10/10 items) // We don't know whether there is more data - but it gives a lower bound - slice.mutateMetaData(metaData -> metaData.setMinimumKnownSize(end)); + slice.updateMinimumKnownSize(end); } } else { - slice.mutateMetaData(metaData -> metaData.setMinimumKnownSize(end)); + slice.updateMinimumKnownSize(end); } currentOffset = 0; } } catch (Exception e) { throw new RuntimeException(e); - } finally { - cacheDataAccessor.unlock(); } +// } finally { +// cacheDataAccessor.unlock(); +// } if (isRhsExhausted) { // Only initialize after unlocking the current cacheDataAccessor @@ -301,10 +332,56 @@ protected void closeCurrentCacheResources() { } @Override - protected void closeIterator() { - closeCurrentCacheResources(); + public void output(IndentedWriter out, SerializationContext sCxt) { + // TODO Auto-generated method stub + + } + + @Override + protected void closeIteratorActual() { inputIter.close(); + closeCurrentCacheResources(); + } - super.closeIterator(); + @Override + protected void requestCancel() { + inputIter.cancel(); } + +// @Override +// public void output(IndentedWriter out, SerializationContext sCxt) { +// // TODO Auto-generated method stub +// +// } + +// @Override +// protected void closeIterator() { +// closeCurrentCacheResources(); +// inputIter.close(); +// +// super.closeIterator(); +// } +// +// @Override +// protected void requestSubCancel() { +// // TODO Auto-generated method stub +// +// } +// +// @Override +// protected void closeSubIterator() { +// closeCurrentCacheResources(); +// } +// +// @Override +// protected boolean hasNextBinding() { +// // TODO Auto-generated method stub +// return false; +// } +// +// @Override +// protected Binding moveToNextBinding() { +// // TODO Auto-generated method stub +// return null; +// } } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/RequestExecutor.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/RequestExecutor.java deleted file mode 100644 index 06c6ac3d909..00000000000 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/RequestExecutor.java +++ /dev/null @@ -1,260 +0,0 @@ -/* - * 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 - * - * https://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. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -package org.apache.jena.sparql.service.enhancer.impl; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.NavigableMap; -import java.util.Set; - -import org.apache.jena.atlas.iterator.IteratorCloseable; -import org.apache.jena.atlas.lib.Closeable; -import org.apache.jena.graph.Node; -import org.apache.jena.sparql.core.Var; -import org.apache.jena.sparql.engine.ExecutionContext; -import org.apache.jena.sparql.engine.QueryIterator; -import org.apache.jena.sparql.engine.binding.Binding; -import org.apache.jena.sparql.engine.binding.BindingFactory; -import org.apache.jena.sparql.engine.iterator.QueryIterConvert; -import org.apache.jena.sparql.engine.iterator.QueryIterPeek; -import org.apache.jena.sparql.engine.iterator.QueryIterPlainWrapper; -import org.apache.jena.sparql.expr.NodeValue; -import org.apache.jena.sparql.graph.NodeTransform; -import org.apache.jena.sparql.service.enhancer.impl.util.BindingUtils; -import org.apache.jena.sparql.service.enhancer.impl.util.QueryIterSlottedBase; -import org.apache.jena.sparql.service.enhancer.impl.util.VarUtilsExtra; -import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerInit; -import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerConstants; - -/** - * Prepare and execute bulk requests - */ -public class RequestExecutor - extends QueryIterSlottedBase -{ - protected OpServiceInfo serviceInfo; - - /** Ensure that at least there are active requests to serve the next n input bindings */ - protected int fetchAhead = 5; - protected int maxRequestSize = 2000; - - protected OpServiceExecutor opExecutor; - protected ExecutionContext execCxt; - protected ServiceResultSizeCache resultSizeCache; - protected ServiceResponseCache cache; - protected CacheMode cacheMode; - - protected IteratorCloseable> batchIterator; - protected Var globalIdxVar; - - // Input iteration - protected long currentInputId = -1; - protected QueryIterPeek activeIter; - - protected Map inputToBinding = new HashMap<>(); - protected Map inputToOutputIt = new LinkedHashMap<>(); - protected Set inputToClose = new HashSet<>(); // Whether an iterator can be closed once the input is processed - - public RequestExecutor( - OpServiceExecutorImpl opExector, - // boolean useLoopJoin, - OpServiceInfo serviceInfo, - ServiceResultSizeCache resultSizeCache, - ServiceResponseCache cache, - CacheMode cacheMode, - IteratorCloseable> batchIterator) { - this.opExecutor = opExector; - // this.useLoopJoin = useLoopJoin; - this.serviceInfo = serviceInfo; - this.resultSizeCache = resultSizeCache; - this.cache = cache; - this.cacheMode = cacheMode; - this.batchIterator = batchIterator; - - // Allocate a fresh index var - services may be nested which results in - // multiple injections of an idxVar which need to be kept separate - Set visibleServiceSubOpVars = serviceInfo.getVisibleSubOpVarsScoped(); - this.globalIdxVar = VarUtilsExtra.freshVar("__idx__", visibleServiceSubOpVars); - this.execCxt = opExector.getExecCxt(); - this.activeIter = QueryIterPeek.create(QueryIterPlainWrapper.create(Collections.emptyList().iterator(), execCxt), execCxt); - } - - @Override - protected Binding moveToNext() { - - Binding parentBinding = null; - Binding childBindingWithIdx = null; - - // Peek the next binding on the active iterator and verify that it maps to the current - // partition key - while (true) { - if (activeIter.hasNext()) { - Binding peek = activeIter.peek(); - long peekOutputId = BindingUtils.getNumber(peek, globalIdxVar).longValue(); - - boolean matchesCurrentPartition = peekOutputId == currentInputId; - - if (matchesCurrentPartition) { - parentBinding = inputToBinding.get(currentInputId); - childBindingWithIdx = activeIter.next(); - break; - } - } - - // Cleanup of no longer needed resources - boolean isClosePoint = inputToClose.contains(currentInputId); - if (isClosePoint) { - QueryIterPeek it = inputToOutputIt.get(currentInputId); - it.close(); - inputToClose.remove(currentInputId); - } - - inputToBinding.remove(currentInputId); - - // Increment rangeId/inputId until we reach the end - ++currentInputId; - - // Check if we need to load the next batch - // If there are missing (=non-loaded) rows within the read ahead range then load them - if (!inputToOutputIt.containsKey(currentInputId)) { - if (batchIterator.hasNext()) { - prepareNextBatchExec(); - } - } - - // If there is still no further batch then we assume we reached the end - if (!inputToOutputIt.containsKey(currentInputId)) { - break; - } - - activeIter = inputToOutputIt.get(currentInputId); - } - - // Remove the idxVar from the childBinding - Binding result = null; - if (childBindingWithIdx != null) { - Binding childBinding = BindingUtils.project(childBindingWithIdx, childBindingWithIdx.vars(), globalIdxVar); - result = BindingFactory.builder(parentBinding).addAll(childBinding).build(); - } - - if (result == null) { - freeResources(); - } - - return result; - } - - /** Prepare the lazy execution of the next batch and register all iterators with {@link #inputToOutputIt} */ - // seqId = sequential number injected into the request - // inputId = id (index) of the input binding - // rangeId = id of the range w.r.t. to the input binding - // partitionKey = (inputId, rangeId) - public void prepareNextBatchExec() { - - GroupedBatch batchRequest = batchIterator.next(); - - // TODO Support ServiceOpts from Node directly - ServiceOpts so = ServiceOpts.getEffectiveService(serviceInfo.getOpService()); - - Node targetServiceNode = so.getTargetService().getService(); - - // Refine the request w.r.t. the cache - Batch batch = batchRequest.getBatch(); - - // This block sets up the execution of the batch - // For aesthetics, bindings are re-numbered starting with 0 when creating the backend request - // These ids are subsequently mapped back to the offset of the input iterator - { - NavigableMap batchItems = batch.getItems(); - - List inputs = new ArrayList<>(batchItems.values()); - - NodeTransform serviceNodeRemapper = node -> ServiceEnhancerInit.resolveServiceNode(node, execCxt); - - Set inputVarsMentioned = BindingUtils.varsMentioned(inputs); - ServiceCacheKeyFactory cacheKeyFactory = ServiceCacheKeyFactory.createCacheKeyFactory(serviceInfo, inputVarsMentioned, serviceNodeRemapper); - - Set visibleServiceSubOpVars = serviceInfo.getVisibleSubOpVarsScoped(); - Var batchIdxVar = VarUtilsExtra.freshVar("__idx__", visibleServiceSubOpVars); - - BatchQueryRewriterBuilder builder = BatchQueryRewriterBuilder.from(serviceInfo, batchIdxVar); - - if (ServiceEnhancerConstants.SELF.equals(targetServiceNode)) { - builder.setOrderRetainingUnion(true) - .setSequentialUnion(true); - } - - BatchQueryRewriter rewriter = builder.build(); - - QueryIterServiceBulk baseIt = new QueryIterServiceBulk( - serviceInfo, rewriter, cacheKeyFactory, opExecutor, execCxt, inputs, - resultSizeCache, cache, cacheMode); - - QueryIterator tmp = baseIt; - - // Remap the local input id of the batch to the global one here - Var innerIdxVar = baseIt.getIdxVar(); - List reverseMap = new ArrayList<>(batchItems.keySet()); - - tmp = new QueryIterConvert(baseIt, b -> { - int localId = BindingUtils.getNumber(b, innerIdxVar).intValue(); - long globalId = reverseMap.get(localId); - - Binding q = BindingUtils.project(b, b.vars(), innerIdxVar); - Binding r = BindingFactory.binding(q, globalIdxVar, NodeValue.makeInteger(globalId).asNode()); - - return r; - }, execCxt); - - - QueryIterPeek queryIter = QueryIterPeek.create(tmp, execCxt); - // Register the iterator with the input ids - // for (int i = 0; i < batchItems.size(); ++i) { - for (Long e : batchItems.keySet()) { - inputToOutputIt.put(e, queryIter); - } - - long lastKey = batch.getItems().lastKey(); - inputToClose.add(lastKey); - } - } - - protected void freeResources() { - for (long inputId : inputToClose) { - Closeable closable = inputToOutputIt.get(inputId); - closable.close(); - } - batchIterator.close(); - } - - @Override - protected void closeIterator() { - freeResources(); - super.closeIterator(); - } -} - diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/RequestExecutorBase.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/RequestExecutorBase.java new file mode 100644 index 00000000000..c72762b1de2 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/RequestExecutorBase.java @@ -0,0 +1,856 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.UnaryOperator; + +import org.apache.jena.atlas.io.IndentedWriter; +import org.apache.jena.atlas.iterator.Iter; +import org.apache.jena.atlas.lib.Closeable; +import org.apache.jena.atlas.lib.Creator; +import org.apache.jena.sparql.serializer.SerializationContext; +import org.apache.jena.sparql.service.enhancer.concurrent.ExecutorServicePool; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterator; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIteratorBase; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIteratorPeek; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterators; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbstractAbortableIterator; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.io.Closer; + +/** A flat map iterator that can read ahead in the input iterator and run flat map operations concurrently. */ +public abstract class RequestExecutorBase + extends AbstractAbortableIterator +{ + private static final Logger logger = LoggerFactory.getLogger(RequestExecutorBase.class); + + /** + * Whether to order output across all batches by the individual items (ITEM) + * or whether each batch is processed as one consecutive unit (BATCH). + * + * For item-level ordering each item must have an ordinal. + */ + public enum Granularity { + ITEM, + BATCH + } + + protected static record InternalBatch(long batchId, boolean isInNewThread, G groupKey, List batch, List reverseMap) {} + + /** + * Transaction-aware interface for the customizing iterator creation. + * The begin and end methods are intended for transaction management. + * + * Each method is only invoked once and all methods are always invoked on the same thread. + * The end method will be invoked upon closing the iterator or if iterator creation fails. + */ + public interface IteratorCreator extends Creator> {} + + static class EnqueTask + implements Runnable + { + public static final Object POISON = new Object(); + + private final IteratorCreator iteratorCreator; + private final UnaryOperator detacher; + private final BlockingQueue queue; + private final Runnable onThreadEnd; + + // The iterator is obtained from the iteratorCreator during run(). + private AbortableIterator iterator; + // private CountDownLatch terminationLatch = new CountDownLatch(1); + + // If an error occurs then it is tracked in this field. + private volatile Throwable throwable; + + private volatile boolean isAborted = false; + private volatile boolean poisonEnqeued = false; + private boolean isTerminated = false; + + public EnqueTask(IteratorCreator iteratorCreator, UnaryOperator detacher, BlockingQueue queue, Runnable onThreadEnd) { + this.iteratorCreator = iteratorCreator; + this.detacher = detacher; + this.queue = queue; + this.onThreadEnd = onThreadEnd; + } + + /** + * Set the flag that the task is considered cancelled. + * Upon the next interrupt all resources will be freed. + * Note that you MUST interrupt the thread to abort it prematurely. + */ + public void setAbortFlag() { + this.isAborted = true; + } + + private void enquePoison() { + if (!poisonEnqeued) { + while (true) { + try { + queue.put(POISON); + poisonEnqeued = true; + break; + } catch (Throwable t) { + logger.warn("Iterrupted while attempting to place POISON on the queue.", t); + } + } + } + } + + public boolean isPoisonEnqueued() { + return poisonEnqeued; + } + + protected void setException(Throwable throwable) { + this.throwable = throwable; + } + + public Throwable getThrowable() { + return throwable; + } + + @Override + public void run() { + // Abort if there already was an error. + if (throwable != null) { + throw new RuntimeException(throwable); + } + + // If the poison was enqueued then all processing is complete - + // regardless whether normally or exceptionally. + if (isPoisonEnqueued()) { + return; + } + + boolean isPaused = false; + try { + runInternal(); + } catch (InterruptedException e) { + if (!isAborted) { + // Exit without exception if interrupted without abort. + isPaused = true; + } else { + setException(new CancellationException()); + } + } catch (Throwable t) { + setException(t); + } finally { + if (!isPaused) { + terminate(); + } + } + } + + public void runInternal() throws InterruptedException { + // If cancelled then don't start the iterator. + if (isAborted) { + throw new CancellationException(); + } + + if (iterator == null) { + iterator = iteratorCreator.create(); + } + + // Always reserve one slot for the poison. + while (iterator.hasNext()) { // Rely on the queue's InterruptedException - no Thread.interrupted() here. + T item = iterator.next(); + T detachedItem = detacher == null ? item : detacher.apply(item); + + // Rely on InterruptedException here. + queue.put(detachedItem); + } + } + + protected void terminate() { + if (!isTerminated) { + isTerminated = true; + try { + if (iterator != null) { + iterator.close(); + } + } finally { + try { + enquePoison(); + } finally { + onThreadEnd.run(); + } + } + + if (throwable != null) { + throw new RuntimeException(throwable); + } + } + } + } + + static class IteratorWrapperViaThread + extends AbstractAbortableIterator + { + protected EnqueTask enqueTask; + + /** + * A function to detach items from the life-cycle of the iterator. + * For example, TDB2 Bindings must be detached from resources that are free'd when the iterator is closed. + */ + protected UnaryOperator detachFn; + + + // protected ExecutorServicePool executorServicePool; + protected ExecutorService executorService; + protected Future future; + + /** + * Deque item type is of type Object in order to be capabable of holding the POISON. + * All other items are of type T. + */ + protected BlockingQueue queue; + protected volatile Throwable throwable = null; + protected volatile List buffer; + + public IteratorWrapperViaThread( + ExecutorService executorSerivce, int maxQueueSize, IteratorCreator iteratorCreator, UnaryOperator detacher, + Runnable onThreadEnd) { + if (maxQueueSize < 1) { + throw new IllegalArgumentException("Queue size must be at least 1."); + } + + this.executorService = executorSerivce; + this.queue = new ArrayBlockingQueue<>(maxQueueSize + 1); // Internally add one for the poison. + this.enqueTask = new EnqueTask<>(iteratorCreator, detacher, queue, onThreadEnd); + // TODO Probably we need a lambda for when the thread exits + // Need to hand the executor service back. + } + + private void start() { + if (!isFinished()) { + if (future == null) { + future = executorService.submit(enqueTask); + } + } + } + + @SuppressWarnings("unchecked") + @Override + public T moveToNext() { + Object item; + try { + item = queue.take(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + T result; + if (item == EnqueTask.POISON) { + result = endOfData(); + Throwable t = enqueTask.getThrowable(); + if (t != null) { + throw new RuntimeException(t); + } + } else { + result = (T)item; + } + return result; + } + + @Override + protected void requestCancel() { + enqueTask.setAbortFlag(); + if (future != null) { + future.cancel(true); + } + } + + public void pause() { + stop(); + } + + @Override + protected void closeIteratorActual() { + // Without the abort flag non-terminated task would only pause and retain their resources. + // Setting the abort flag ensures that the task terminates upon interruption and frees its resources. + enqueTask.setAbortFlag(); + stop(); + } + + private void stop() { + if (future != null) { + // XXX Java limitation: future.cancel(true) followed by future.get() does not + // wait for the thread to exit. + future.cancel(true); + try { + future.get(); + } catch (InterruptedException | ExecutionException | CancellationException e) { + // Ignore + } catch (Throwable e) { + throw new RuntimeException(e); + } + /* + try { + enqueTask.getTerminationLatch().await(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + */ + future = null; + } + } + + @Override + public void output(IndentedWriter out, SerializationContext sCxt) { + // TODO Auto-generated method stub + } + } + + protected static long getCloseId(Granularity granularity, TaskEntry taskEntry) { + long result = switch (granularity) { + case ITEM -> taskEntry.getServedInputIds().get(taskEntry.getServedInputIds().size() - 1); + case BATCH -> taskEntry.getBatchId(); + }; + return result; + } + + interface TaskEntry + extends AutoCloseable + { + long getBatchId(); + List getServedInputIds(); + AbortableIteratorPeek stopAndGet(); + @Override void close(); + } + + class TaskEntryEmpty + implements TaskEntry + { + private long closeId; + private List servedInputIds; + + public TaskEntryEmpty(long closeId) { + super(); + this.closeId = closeId; + this.servedInputIds = List.of(closeId); + } + + @Override + public long getBatchId() { + return closeId; + } + + @Override + public List getServedInputIds() { + return servedInputIds; + } + + @Override + public AbortableIteratorPeek stopAndGet() { + return new AbortableIteratorPeek<>(AbortableIterators.empty()); + } + + @Override + public void close() {} + } + + abstract class TaskEntryBatchBase + implements TaskEntry + { + protected InternalBatch batch; + + public TaskEntryBatchBase(InternalBatch batch) { + super(); + this.batch = batch; + } + + @Override + public long getBatchId() { + return batch.batchId(); + } + + @Override + public List getServedInputIds() { + return batch.reverseMap(); + } + } + + class TaskEntryDirect + extends TaskEntryBatchBase + { + protected IteratorCreator iteratorCreator; + protected AbortableIteratorPeek createdIterator; + + public TaskEntryDirect(InternalBatch batch) { + super(batch); + } + + @Override + public AbortableIteratorPeek stopAndGet() { + if (iteratorCreator == null) { + iteratorCreator = processBatch(batch); + AbortableIterator it = iteratorCreator.create(); + createdIterator = new AbortableIteratorPeek<>(it); + } + return createdIterator; + } + + @Override + public void close() { + Iter.close(createdIterator); + } + } + + // There are three ways to execute sub-iterators: + // EmptyTask - a dummy task that has a closeId but does not produce items + // DirectTask - return an iterator directly on the driver thread + // AsyncTask - items are prefetched until the driver calls "stop". for scattered data the driver could actually restart the fetch-ahead again... but let's not do that now. probably it would be relatively easy to add later. + + /** Helper class for tracking running prefetch tasks. */ + // TODO Free the task once the thread exits. + // What happens if the task itself gives back the executor?! -> The executor will be simply handed back to the pool, but the task will exit only shortly after. + // This means that the executor may be briefly occupied which should be harmless. + class TaskEntryAsync + extends TaskEntryBatchBase + { + protected final ExecutorService executorService; + protected final Runnable onThreadEnd; + + // Volatile because iterator can be created by any thread. + protected volatile AbortableIteratorPeek peekIter; + + public TaskEntryAsync(InternalBatch batch, ExecutorService executorService, Runnable onThreadEnd) { + super(batch); + this.executorService = executorService; + this.onThreadEnd = onThreadEnd; + } + + // protected volatile boolean isFreed = false; + public synchronized void startInNewThread() { + if (peekIter == null) { + boolean isInNewThread = true; + + IteratorCreator creator = processBatch(batch); + UnaryOperator detacher = x -> detachOutput(x, isInNewThread); + IteratorWrapperViaThread threadIt = new IteratorWrapperViaThread<>( + executorService, maxBufferAhead, creator, detacher, onThreadEnd); + + // Start the thread on the iterator. + threadIt.start(); + peekIter = new AbortableIteratorPeek<>(threadIt); + } + } + + /** + * Stop the task and return an iterator over the buffered items and the remaining ones. + * Closing the returned iterator also closes the iterator over the remaining items. + * + * In case of an exception, this task entry closes itself. + */ + // Synchronized to protect against the case when the driver wants to consume data from a task before + // that task was started from the taskQueue. + @Override + public synchronized AbortableIteratorPeek stopAndGet() { + startInNewThread(); + return peekIter; + } + + @Override + public synchronized void close() { + startInNewThread(); + // Make sure to close peekIter. + // if (peekIter != null) { + peekIter.close(); + // } + } + } + + /** Ensure that at least there are active requests to serve the next n input bindings */ + protected int maxFetchAhead = 100; // Fetch ahead is for additional task slots once maxConcurrentTasks have completed. + + /** + * Only the driver is allowed to read from batchIterator because of a possibly running transaction. + * Buffer ahead is how many items to read (and detach) from batchIterator so that new tasks can be scheduled + * asynchronously (i.e. without the driver thread having to interfere). + */ + protected int maxBufferAhead = 100; + + /** If batch granularity is true then output is ordered according to the obtained batches. + * Otherwise, output is ordered according to the individual member ids of the batches (assumes ids are unique across batches). */ + protected Granularity granularity; + protected AbortableIterator> batchIterator; + + protected long nextBatchId = 0; // Counter of items taken from batchIterator + + // Input iteration. + protected long currentInputId = -1; + protected TaskEntry activeTaskEntry = null; // Cached reference for inputToOutputIt.get(currentInputId). + protected AbortableIteratorPeek activeIter; + + /** + * Tasks ordered by their input id - regardless of whether they are run concurrently or not. + * The complexity here is that the multiple inputIds may map to the same task. + * Conversely: A single task may serve multiple input ids. + */ + protected Map> inputToOutputIt = new LinkedHashMap<>(); + + /** The task queue is used to submit the tasks in "inputToOutputIt" to executors. */ + protected AtomicInteger taskQueueCapacity; + protected Deque taskQueue = new ArrayDeque<>(); + + /* State for tracking concurrent prefetch ----------------------------- */ + + protected ExecutorServicePool executorServicePool; + + private final int maxConcurrentTasks; + private final long concurrentSlotReadAheadCount; + + // Whenever a task is started, the count is incremented. + // Tasks decrement the count themselves just before exiting. + private final AtomicInteger freeWorkerSlots = new AtomicInteger(); + + private Meter throughputMeter = new Meter(5); + // private Deque> throughputHistory = new ArrayDeque<>(5); + // private AtomicInteger completedTasksSinceLastNext = new AtomicInteger(); + // private int numRecentScheduledTasks; + + /** The concurrently running tasks. Indexed once by the last input id (upon which to close). */ + // private final Map>> openConcurrentTaskEntriesRunning = new ConcurrentHashMap<>(); + + /** + * Completed tasks are moved from the running map to this one by the driver thread. + * This map is used to count the number of completed tasks and the remaining task slots. + * This map is not used for resource management. + */ + // private final Map>> openConcurrentTaskEntriesCompleted = new ConcurrentHashMap<>(); + + /* Actual implementation ---------------------------------------------- */ + + public RequestExecutorBase( + AtomicBoolean cancelSignal, + Granularity granularity, + AbortableIterator> batchIterator, + int maxConcurrentTasks, + long concurrentSlotReadAheadCount) { + super(cancelSignal); + this.granularity = Objects.requireNonNull(granularity); + this.batchIterator = Objects.requireNonNull(batchIterator); + + // Set up a dummy task with an empty iterator as the active one + // (currentInputId set to -1) and ensure it gets properly closed. + activeTaskEntry = new TaskEntryEmpty(currentInputId); // emptyTaskEntry(currentInputId); //TaskEntry.empty(currentInputId); + inputToOutputIt.put(currentInputId, activeTaskEntry); + + this.activeIter = activeTaskEntry.stopAndGet(); + this.maxConcurrentTasks = maxConcurrentTasks; + this.concurrentSlotReadAheadCount = concurrentSlotReadAheadCount; + + this.executorServicePool = new ExecutorServicePool(); + + this.freeWorkerSlots.set(maxConcurrentTasks); + this.taskQueueCapacity = new AtomicInteger(maxConcurrentTasks * 2); + } + + + protected void autoScale() { + // XXX Scale the task queue size based on the number of processed items since last time coming here. + // I think we want the maximum number of threads that finish per tick (in a window). + // throughputMeter.tick(); + + // If the task queue is empty then scale up the size of it. + if (taskQueue.isEmpty()) { + // TODO get current max capacity and double it. + // taskQueueCapacity. + } + + // If the task queue is non-empty then we can measure the number of + // We can actually measure the min/max/avg time spent per concurrent thread until it was finished. + // We can use this to scale the task queue and the threads at the same time + // If more threads make it slower then we scale the thread count down again + // through we might need to take into account whether a producer thread was blocked on the queue... + + + } + + @Override + protected O moveToNext() { + O result = null; + + // Check whether to scale up or down the size of the task queue or the number of workers. + autoScale(); + + boolean didAdvanceActiveIter = false; + // Peek the next binding on the active iterator and verify that it maps to the current + // partition key. + while (true) { // Note: Cancellation is handled by base class before calling moveToNext(). + if (activeIter.hasNext()) { + O peek = activeIter.peek(); + // The iterator returns null if it was aborted. + if (peek == null) { + break; + } + + // On batch granularity always take the lowest input id served by a task. + long peekOutputId = switch(granularity) { + case ITEM -> extractInputOrdinal(peek); + case BATCH -> activeTaskEntry.getBatchId(); + }; + + boolean inputIdMatches = peekOutputId == currentInputId; + if (inputIdMatches) { + result = activeIter.next(); + break; + } + } + + // XXX Potential optimization: If activeIter of activeTask is not yet consumed then prefetching could be restarted. + + // If we come here then we need to advance the lhs. + didAdvanceActiveIter = true; + + // Free up no longer needed resources. + long closeId = getCloseId(granularity, activeTaskEntry); + boolean isClosePoint = currentInputId == closeId; + if (isClosePoint) { + activeIter.close(); + activeTaskEntry.close(); + } + + // Remote the just processed entry for currentInputId. + inputToOutputIt.remove(currentInputId); + + // Move to the next inputId + ++currentInputId; // TODO peekOutputId may not have matched currentInputId - + // in this case we still get an entry in inputToOutputIt but the iterator will be empty. + + activeTaskEntry = inputToOutputIt.get(currentInputId); + if (activeTaskEntry == null) { + // No entry for the advanced inputId - check whether further batches need to be executed. + prepareNextBatchExecs(); + + activeTaskEntry = inputToOutputIt.get(currentInputId); + + // If there is still no further batch then we must have reached the end. + if (activeTaskEntry == null) { + break; + } + } + + // Stop any concurrent prefetching and get the iterator. + activeIter = activeTaskEntry.stopAndGet(); + } + + if (result == null) { + result = endOfData(); + } else { + if (!didAdvanceActiveIter) { + // Check whether to schedule any further concurrent tasks. + // Check was already performed if activeIter was advanced. + prepareNextBatchExecs(); + } + } + + return result; + } + + protected void registerTaskEntry(TaskEntry taskEntry) { + switch (granularity) { + case ITEM: + List servedInputIds = taskEntry.getServedInputIds(); + for (Long e : servedInputIds) { + inputToOutputIt.put(e, taskEntry); + } + break; + case BATCH: + long batchId = taskEntry.getBatchId(); + inputToOutputIt.put(batchId, taskEntry); + break; + default: + throw new IllegalStateException("Should never come here."); + } + } + + public void prepareNextBatchExecs() { + // FIXME There may be a task in the task queue. + // Ideally the driver would already create the entry in inputToOutputMap. + // and the executors would just call a start() method on the tasks. + + if (!inputToOutputIt.containsKey(currentInputId)) { + // We need the task's iterator right away - do not start concurrent retrieval + if (batchIterator.hasNext()) { + InternalBatch batch = nextBatch(false); + TaskEntry taskEntry = new TaskEntryDirect(batch); + registerTaskEntry(taskEntry); + } + } + + fillTaskQueue(); + processTaskQueue(); + } + + /** This method is only called from the driver - but worker threads may take items from the queue. */ + protected void fillTaskQueue() { + // Fail to schedule tasks if promoted to a write transaction. + checkCanExecInNewThread(); + + // TODO Do not fill the task queue when aborted. + while (batchIterator.hasNext()) { + // Check the capacity of the task queue before . + int remainingCapacity = taskQueueCapacity.getAndUpdate(i -> Math.max(0, i - 1)); + if (remainingCapacity == 0) { + break; + } + + // Set up tasks that will be run asynchronously. + InternalBatch batch = nextBatch(true); + ExecutorService executorService = executorServicePool.acquireExecutor(); + Runnable onThreadEnd = () -> { + processTaskQueue(); + // Note: This lambda is run from the executor. + // Shutting the executor down hands it back to the executor pool. + // If the executor is meanwhile re-acquired it will be briefly blocked + // until this method exits. This should be harmless. + executorService.shutdown(); + freeWorkerSlots.incrementAndGet(); + taskQueueCapacity.incrementAndGet(); + +// System.out.println("freeWorkerSlots: " + freeWorkerSlots.get()); +// System.out.println("taskQueueCapacity: " + taskQueueCapacity.get()); + }; + + TaskEntryAsync taskEntry = new TaskEntryAsync(batch, executorService, onThreadEnd); + registerTaskEntry(taskEntry); + taskQueue.add(taskEntry); + } + } + + /** Method called from driver or worker threads to start execution of tasks in the queue. */ + protected void processTaskQueue() { + // System.err.println("Task queue size: " + taskQueue.size()); + // If there are more than 0 free slots then decrement the count; otherwise stay at 0. + int freeSlots; + while ((freeSlots = freeWorkerSlots.getAndUpdate(i -> Math.max(0, i - 1))) > 0) { + // Need to ensure the queue is not empty. + TaskEntryAsync taskEntry = taskQueue.poll(); + if (taskEntry == null) { + // Nothing to execute - free the slot again + freeWorkerSlots.incrementAndGet(); + break; + } + + // Launch the task. The task is set up to call "freeTaskSlots.incrementAndGet()" upon completion. + taskEntry.startInNewThread(); + } + } + + protected abstract boolean isCancelled(); + + protected IteratorCreator processBatch(InternalBatch batch) { + boolean isInNewThread = batch.isInNewThread(); + // long batchId = batch.batchId(); + G groupKey = batch.groupKey(); + List inputs = batch.batch(); + List reverseMap = batch.reverseMap(); + IteratorCreator result = processBatch(isInNewThread, groupKey, inputs, reverseMap); + return result; + } + + protected abstract IteratorCreator processBatch(boolean isInNewThread, G groupKey, List batch, List reverseMap); + + protected abstract long extractInputOrdinal(O input); + protected abstract void checkCanExecInNewThread(); + + protected I detachInput(I item, boolean isInNewThread) { return item; } + protected O detachOutput(O item, boolean isInNewThread) { return item; } + + /** Consume the next batch from the batchIterator. */ + protected InternalBatch nextBatch(boolean isInNewThread) { + GroupedBatch batchRequest = batchIterator.next(); + long batchId = nextBatchId++; + Batch batch = batchRequest.getBatch(); + NavigableMap batchItems = batch.getItems(); + + G groupKey = batchRequest.getGroupKey(); + + // Detach inputs. + Collection rawInputs = batchItems.values(); + List detachedInputs = new ArrayList<>(rawInputs.size()); + rawInputs.forEach(rawInput -> detachedInputs.add(detachInput(rawInput, isInNewThread))); + List inputs = Collections.unmodifiableList(detachedInputs); + + List reverseMap = List.copyOf(batchItems.keySet()); + + InternalBatch result = new InternalBatch<>(batchId, isInNewThread, groupKey, inputs, reverseMap); + return result; + } + + protected void freeResources() { + // Use closer to free as much as possible in case of failure. + try (Closer closer = Closer.create()) { + closer.register(activeIter::close); + + // TODO The some objects may get closed multiple times - because the same task may be registered under multiple + // input ids - use a IdentityHashSet(values())??? + for (TaskEntry taskEntry : inputToOutputIt.values()) { + Closeable closable = taskEntry.stopAndGet(); + closer.register(closable::close); + closer.register(taskEntry::close); + } + + closer.register(batchIterator::close); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + protected void closeIteratorActual() { + freeResources(); + if (false) { + System.out.println("final taskQueueSize: " + taskQueue.size()); + System.out.println("final freeWorkerSlots: " + freeWorkerSlots.get()); + System.out.println("final taskQueueCapacity: " + taskQueueCapacity.get()); + } + } + + @Override + protected void requestCancel() { + batchIterator.cancel(); + AbortableIteratorBase.performRequestCancel(activeIter); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/RequestExecutorBulkAndCache.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/RequestExecutorBulkAndCache.java new file mode 100644 index 00000000000..2ec6ec56ac0 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/RequestExecutorBulkAndCache.java @@ -0,0 +1,138 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl; + +import java.util.List; +import java.util.Set; + +import org.apache.jena.graph.Node; +import org.apache.jena.sparql.core.Var; +import org.apache.jena.sparql.engine.ExecutionContext; +import org.apache.jena.sparql.engine.QueryIterator; +import org.apache.jena.sparql.engine.binding.Binding; +import org.apache.jena.sparql.engine.binding.BindingFactory; +import org.apache.jena.sparql.engine.iterator.QueryIterConvert; +import org.apache.jena.sparql.expr.NodeValue; +import org.apache.jena.sparql.graph.NodeTransform; +import org.apache.jena.sparql.service.enhancer.impl.util.BindingUtils; +import org.apache.jena.sparql.service.enhancer.impl.util.VarUtilsExtra; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterator; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterators; +import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerConstants; +import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerInit; + +/** + * This class is an iterator that handles partitioning bulk requests and caching. + * The class {@link QueryIterServiceBulkAndCache} is responsible to process a concrete batch. + */ +public class RequestExecutorBulkAndCache + extends RequestExecutorSparqlBase +{ + protected OpServiceExecutor opExecutor; + protected OpServiceInfo serviceInfo; + + protected ServiceResultSizeCache resultSizeCache; + protected ServiceResponseCache cache; + protected CacheMode cacheMode; + + protected Var globalIdxVar; + + public RequestExecutorBulkAndCache( + AbortableIterator> batchIterator, + int maxConcurrentTasks, + long concurrentSlotReadAheadCount, + ExecutionContext execCxt, + OpServiceExecutorImpl opExector, + OpServiceInfo serviceInfo, + ServiceResultSizeCache resultSizeCache, + ServiceResponseCache cache, + CacheMode cacheMode) { + super(Granularity.ITEM, batchIterator, maxConcurrentTasks, concurrentSlotReadAheadCount, execCxt); + this.opExecutor = opExector; + this.serviceInfo = serviceInfo; + this.resultSizeCache = resultSizeCache; + this.cache = cache; + this.cacheMode = cacheMode; + + // Allocate a fresh index var - services may be nested which results in + // multiple injections of an idxVar which needs to be kept separate + Set visibleServiceSubOpVars = serviceInfo.getVisibleSubOpVarsScoped(); + this.globalIdxVar = VarUtilsExtra.freshVar("__idx__", visibleServiceSubOpVars); + } + + @Override + public AbortableIterator buildIterator(boolean runsOnNewThread, Node groupKey, List inputs, List reverseMap, ExecutionContext batchExecCxt) { + ServiceOpts so = ServiceOptsSE.getEffectiveService(serviceInfo.getOpService()); + Node targetServiceNode = so.getTargetService().getService(); + + NodeTransform serviceNodeRemapper = node -> ServiceEnhancerInit.resolveServiceNode(node, batchExecCxt); + + Set inputVarsMentioned = BindingUtils.varsMentioned(inputs); + ServiceCacheKeyFactory cacheKeyFactory = ServiceCacheKeyFactory.createCacheKeyFactory(serviceInfo, inputVarsMentioned, serviceNodeRemapper); + + BatchQueryRewriterBuilder builder = BatchQueryRewriterBuilder.from(serviceInfo, globalIdxVar); + + if (ServiceEnhancerConstants.SELF.equals(targetServiceNode)) { + builder + .setOrderRetainingUnion(true) + .setSequentialUnion(true); + } + + BatchQueryRewriter rewriter = builder.build(); + + QueryIterServiceBulkAndCache baseIt = new QueryIterServiceBulkAndCache( + serviceInfo, rewriter, cacheKeyFactory, opExecutor, batchExecCxt, inputs, + resultSizeCache, cache, cacheMode); + + QueryIterator tmp = baseIt; + + // Remap the local input id of the batch to the global one here + Var innerIdxVar = baseIt.getIdxVar(); + + tmp = new QueryIterConvert(baseIt, b -> { + int localId = BindingUtils.getNumber(b, innerIdxVar).intValue(); + long globalId = reverseMap.get(localId); + + Binding q = BindingUtils.project(b, b.vars(), innerIdxVar); + Binding r = BindingFactory.binding(q, globalIdxVar, NodeValue.makeInteger(globalId).asNode()); + + return r; + }, batchExecCxt); + + return AbortableIterators.adapt(tmp); + } + + @Override + protected long extractInputOrdinal(Binding input) { + // Even if the binding is otherwise empty the ID for globalIdxVar must never be null! + long result = BindingUtils.getNumber(input, globalIdxVar).longValue(); + return result; + } + + /** Extend super.moveToNext to exclude the internal globalIdxVar from the bindings. */ + @Override + protected Binding moveToNext() { + Binding tmp = super.moveToNext(); + Binding result = tmp == null ? null : BindingUtils.project(tmp, tmp.vars(), globalIdxVar); + return result; + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/RequestExecutorSparqlBase.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/RequestExecutorSparqlBase.java new file mode 100644 index 00000000000..1b975e49a1a --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/RequestExecutorSparqlBase.java @@ -0,0 +1,173 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.jena.atlas.io.IndentedWriter; +import org.apache.jena.atlas.lib.Lib; +import org.apache.jena.graph.Node; +import org.apache.jena.query.ReadWrite; +import org.apache.jena.query.TxnType; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.core.Transactional; +import org.apache.jena.sparql.engine.ExecutionContext; +import org.apache.jena.sparql.engine.binding.Binding; +import org.apache.jena.sparql.serializer.SerializationContext; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterator; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterators; +import org.apache.jena.system.TxnOp; + +/** Adaption of the generic {@link RequestExecutorBase} base class to Jena's QueryIterator machinery. */ +public abstract class RequestExecutorSparqlBase + extends RequestExecutorBase +{ + protected ExecutionContext execCxt; + + public RequestExecutorSparqlBase( + Granularity granularity, + AbortableIterator> batchIterator, + int maxConcurrentTasks, + long concurrentSlotReadAheadCount, + ExecutionContext execCxt + ) { + super(execCxt.getCancelSignal(), granularity, batchIterator, maxConcurrentTasks, concurrentSlotReadAheadCount); + this.execCxt = Objects.requireNonNull(execCxt); + } + + @Override + protected void checkCanExecInNewThread() { + DatasetGraph dataset = execCxt.getDataset(); + if (dataset.supportsTransactions()) { + if (dataset.isInTransaction()) { + ReadWrite txnMode = dataset.transactionMode(); + if (ReadWrite.WRITE.equals(txnMode)) { + throw new IllegalStateException("Cannot create concurrent tasks when in a write transaction."); + } + } + } + } + + @Override + public void output(IndentedWriter out, SerializationContext sCxt) { + out.println(Lib.className(this)) ; +// out.incIndent() ; +// for ( QueryIterator qIter : execCxt.listAllIterators() ) +// { +// qIter.output( out, sCxt ); +// } +// out.decIndent() ; + out.ensureStartOfLine() ; + } + + @Override + protected boolean isCancelled() { + AtomicBoolean cancelSignal = execCxt.getCancelSignal(); + return (cancelSignal != null && cancelSignal.get()) || Thread.interrupted(); + } + + @Override + protected Binding detachOutput(Binding item, boolean isInNewThread) { + Binding result = isInNewThread ? item.detach() : item; + return result; + } + + /** Factory method for iterators. May be invoked from different threads. */ + protected abstract AbortableIterator buildIterator(boolean runsOnNewThread, Node groupKey, List inputs, List reverseMap, ExecutionContext batchExecCxt); + + @Override + protected IteratorCreator processBatch(boolean runsOnNewThread, Node groupKey, List inputs, List reverseMap) { + IteratorCreator result; + if (!runsOnNewThread) { + result = new IteratorCreator<>() { + @Override + public AbortableIterator create() { + return buildIterator(runsOnNewThread, groupKey, inputs, reverseMap, execCxt); + } + }; + } else { + ExecutionContext isolatedExecCxt = ExecutionContext.fromFunctionEnv(execCxt); + // TODO Check that fromFunctionEnv is a suitable replacement for the deprecated ctor below: + // ExecutionContext isolatedExecCxt = new ExecutionContext(execCxt.getContext(), execCxt.getActiveGraph(), execCxt.getDataset(), execCxt.getExecutor()); + result = new IteratorCreatorWithTxn<>(isolatedExecCxt, TxnType.READ) { + @Override + protected AbortableIterator createIterator() { + // Note: execCxt in here is assigned to isolatedExecCxt! + return buildIterator(runsOnNewThread, groupKey, inputs, reverseMap, execCxt); + } + }; + } + return result; + } + + static abstract class IteratorCreatorWithTxn + implements IteratorCreator + { + protected ExecutionContext execCxt; + protected TxnType txnType; + + public IteratorCreatorWithTxn(ExecutionContext execCxt, TxnType txnType) { + super(); + this.execCxt = execCxt; + this.txnType = txnType; + } + + @Override + public final AbortableIterator create() { + begin(); + AbortableIterator it = createIterator(); + return AbortableIterators.onClose(it, this::end); + } + + protected abstract AbortableIterator createIterator(); + + protected void begin() { + DatasetGraph dsg = execCxt.getDataset(); + txnBegin(dsg, txnType); + } + + protected void end() { + DatasetGraph dsg = execCxt.getDataset(); + txnEnd(dsg); + } + } + + protected static void txnBegin(Transactional txn, TxnType txnType) { + boolean b = txn.isInTransaction(); + if ( b ) + TxnOp.compatibleWithPromote(txnType, txn); + else + txn.begin(txnType); + } + + protected static void txnEnd(Transactional txn) { + boolean b = txn.isInTransaction(); + if ( !b ) { + if ( txn.isInTransaction() ) + // May have been explicit commit or abort. + txn.commit(); + txn.end(); + } + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceCacheKeyFactory.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceCacheKeyFactory.java index c165fcbccc0..8af68fca2be 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceCacheKeyFactory.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceCacheKeyFactory.java @@ -25,7 +25,6 @@ import java.util.Set; import java.util.stream.Collectors; -import com.google.common.collect.Sets; import org.apache.jena.graph.Node; import org.apache.jena.sparql.algebra.Op; import org.apache.jena.sparql.core.Substitute; @@ -35,6 +34,8 @@ import org.apache.jena.sparql.engine.binding.BindingFactory; import org.apache.jena.sparql.graph.NodeTransform; +import com.google.common.collect.Sets; + public class ServiceCacheKeyFactory { // Needed to resolve 'self' references diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceCacheValue.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceCacheValue.java index b7bd24997e8..8e4a8b50ee8 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceCacheValue.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceCacheValue.java @@ -44,4 +44,9 @@ public long getId() { public Slice getSlice() { return slice; } + + @Override + public String toString() { + return "ServiceCacheValue [id=" + id + ", slice=" + slice + "]"; + } } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceOpts.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceOpts.java index bd42044a7fc..37e4360da1e 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceOpts.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceOpts.java @@ -24,13 +24,12 @@ import java.util.AbstractMap.SimpleEntry; import java.util.ArrayList; import java.util.Iterator; -import java.util.LinkedHashSet; import java.util.List; -import java.util.ListIterator; import java.util.Map.Entry; import java.util.Objects; import java.util.Optional; -import java.util.Set; +import java.util.function.Predicate; +import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -38,7 +37,6 @@ import org.apache.jena.graph.NodeFactory; import org.apache.jena.sparql.algebra.Op; import org.apache.jena.sparql.algebra.op.OpService; -import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerConstants; /** * Utilities to exploit url scheme pattern to represent key value pairs. @@ -58,19 +56,11 @@ * */ public class ServiceOpts { - // Use ':' as a separator unless it is preceeded by the escape char '\' - private static final Pattern SPLIT_PATTERN = Pattern.compile("(?> options; @@ -141,7 +131,7 @@ public OpService toService() { result = opService; } else { Node node = opService.getService(); - String prefixStr = ServiceOpts.unparse(options); + String prefixStr = ServiceOpts.unparseOptions(options); if (!node.isURI()) { Node uri = NodeFactory.createURI(prefixStr); result = new OpService(uri, opService, false); @@ -158,69 +148,92 @@ public String toString() { return "ServiceOpts [options=" + options + ", opService=" + opService + "]"; } - public static List> parseAsOptions(Node node) { - String iri = node.isURI() ? node.getURI() : null; - List> result = iri == null ? null : parseAsOptions(iri); - return result; + public static List> parseEntries(Node node) { + return node.isURI() ? parseEntries(node.getURI()) : null; } /** Split an iri by ':' and attempt to parse the splits as key=value pairs. */ - public static List> parseAsOptions(String iri) { - List> result = new ArrayList<>(); - String[] rawSplits = SPLIT_PATTERN.split(iri); - for (String rawSplit : rawSplits) { - String split = rawSplit.replace("\\\\", "\\"); - String[] kv = split.split("\\+", 2); - result.add(new SimpleEntry<>(kv[0], kv.length == 2 ? kv[1] : null)); + public static List parseEntriesRaw(String iri) { + List result = new ArrayList<>(); + Matcher matcher = SPLIT_PATTERN.matcher(iri); + int nextEntryStart = 0; + int group = 1; // The separator is in this group + while (matcher.find()) { + int delimStart = matcher.start(group); + int delimEnd = matcher.end(group); + if (delimStart > nextEntryStart) { + String entryStr = iri.substring(nextEntryStart, delimStart); + result.add(entryStr); + } + String delimStr = iri.substring(delimStart, delimEnd); + result.add(delimStr); + nextEntryStart = delimEnd; + } + // Add the entry after the last separator (if there is one) + int n = iri.length(); + if (nextEntryStart < n) { + String entryStr = iri.substring(nextEntryStart, n); + result.add(entryStr); } - return result; } - public static String escape(String str) { - String result = str.replace("\\", "\\\\").replace(":", "\\:"); + /** Split an iri by ':' and attempt to parse the splits as key=value pairs. */ + public static List> parseEntries(String iri) { + List rawSplits = parseEntriesRaw(iri); + List> result = rawSplits.stream().map(rawSplit -> { + String split = rawSplit.equals(":") ? "" : rawSplit.replace("::", ":"); + String[] kv = split.split("\\+", 2); + return new SimpleEntry<>(kv[0], kv.length == 2 ? kv[1] : null); + }).collect(Collectors.toList()); return result; } - /** Convert a list of options back into an escaped string */ - public static String unparse(List> optionList) { - String result = optionList.stream() - .map(e -> escape(e.getKey()) + (Optional.ofNullable(e.getValue()).map(v -> "+" + escape(v)).orElse(""))) - .collect(Collectors.joining(":")); + public static boolean isSeparator(String key) { + return "".equals(key); + } - ListIterator> it = optionList.listIterator(optionList.size()); - if (it.hasPrevious()) { - Entry lastEntry = it.previous(); - if (isKnownOption(lastEntry.getKey())) { - result += ":"; - } - } + public static String escape(String str) { + String result = isSeparator(str) ? ":" : str.replace(":", "::"); + return result; + } - //.collect(Collectors.joining(":")); + /** Convert a list of entries back into an escaped string. */ + public static String unparseEntries(List> entryList) { + String result = entryList.stream() + .map(ServiceOpts::unparseEntry) + .collect(Collectors.joining()); return result; } - public static boolean isKnownOption(String key) { - Set knownOptions = new LinkedHashSet<>(); - knownOptions.add(SO_CACHE); - knownOptions.add(SO_BULK); - knownOptions.add(SO_LOOP); - knownOptions.add(SO_OPTIMIZE); + public static String unparseEntry(Entry e) { + String result = escape(e.getKey()) + Optional.ofNullable(e.getValue()).map(v -> "+" + escape(v)).orElse(""); + return result; + } - return knownOptions.contains(key); + /** + * Differs from {@link #unparseEntries(List)} that the input is expected to be free from separators. + */ + public static String unparseOptions(List> optionList) { + String result = optionList.stream() + .map(ServiceOpts::unparseEntry) + .map(x -> x + ":") + .collect(Collectors.joining()); + return result; } - public static ServiceOpts getEffectiveService(OpService opService) { + public static ServiceOpts getEffectiveService(OpService opService, String fallbackServiceIri, Predicate isKnownOption) { List> opts = new ArrayList<>(); OpService currentOp = opService; boolean isSilent; String serviceStr = null; + // The outer loop descends into sub OpService instances as long as options are known + // SERVICE { } is equivalent to SERVICE { SERVICE { } } while (true) { isSilent = currentOp.getSilent(); Node node = currentOp.getService(); - List> parts = ServiceOpts.parseAsOptions(node); - + List> parts = ServiceOpts.parseEntries(node); if (parts == null) { // node is not an iri break; } @@ -232,22 +245,28 @@ public static ServiceOpts getEffectiveService(OpService opService) { for (; i < n; ++i) { Entry e = parts.get(i); String key = e.getKey(); - - if (isKnownOption(key)) { + if (isKnownOption.test(key)) { opts.add(e); + // Skip over the next separator (if there is one) + int j = i + 1; + if (j < n) { + String nextKey = parts.get(j).getKey(); + isSeparator(nextKey); + i = j; + } } else { break; } } List> subList = parts.subList(i, n); - serviceStr = ServiceOpts.unparse(subList); + serviceStr = ServiceOpts.unparseEntries(subList); if (serviceStr.isEmpty()) { Op subOp = opService.getSubOp(); if (subOp instanceof OpService) { currentOp = (OpService)subOp; } else { - serviceStr = ServiceEnhancerConstants.SELF.getURI(); + serviceStr = fallbackServiceIri; break; } } else { diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceOptsSE.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceOptsSE.java new file mode 100644 index 00000000000..30cee94e9cf --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceOptsSE.java @@ -0,0 +1,68 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl; + +import java.util.Set; + +import org.apache.jena.sparql.algebra.op.OpService; +import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerConstants; + +/** Domain adaption for the ServiceEnhancer executor. */ +public class ServiceOptsSE { + + public static final String SO_BULK = "bulk"; + public static final String SO_CACHE = "cache"; + + /** Reserved; currently not implemented */ + public static final String SO_LATERAL = "lateral"; + + public static final String SO_LOOP = "loop"; + + /** + * Modifies loop to substitute only in-scope variables on the rhs. + * Original behavior is to substitute variables regardless of scope. Usage: SERVICE >loop+scoped< {}. + */ + public static final String SO_LOOP_MODE_SCOPED = "scoped"; + + // public static final String SO_CONCURRENT = "concurrent"; + public static final String SO_OPTIMIZE = "optimize"; + + private static Set knownOptions = Set.of( + SO_BULK, + SO_CACHE, + SO_LATERAL, + SO_LOOP, + // SO_CONCURRENT, + SO_OPTIMIZE); + + public Set getKnownOptions() { + return knownOptions; + } + + private static boolean isKnownOption(String key) { + return knownOptions.contains(key); + } + + public static ServiceOpts getEffectiveService(OpService opService) { + return ServiceOpts.getEffectiveService(opService, ServiceEnhancerConstants.SELF.getURI(), ServiceOptsSE::isKnownOption); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceResponseCache.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceResponseCache.java index 71e4a065c5f..532410e47a3 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceResponseCache.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceResponseCache.java @@ -24,21 +24,28 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Supplier; -import org.apache.jena.atlas.logging.Log; -import com.google.common.cache.CacheBuilder; import org.apache.jena.query.ARQ; import org.apache.jena.sparql.engine.binding.Binding; import org.apache.jena.sparql.service.enhancer.claimingcache.AsyncClaimingCache; -import org.apache.jena.sparql.service.enhancer.claimingcache.AsyncClaimingCacheImplGuava; +import org.apache.jena.sparql.service.enhancer.claimingcache.AsyncClaimingCacheImplCaffeine; import org.apache.jena.sparql.service.enhancer.claimingcache.RefFuture; +import org.apache.jena.sparql.service.enhancer.impl.util.Lazy; import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerConstants; import org.apache.jena.sparql.service.enhancer.slice.api.ArrayOps; import org.apache.jena.sparql.service.enhancer.slice.api.Slice; import org.apache.jena.sparql.service.enhancer.slice.impl.SliceInMemoryCache; import org.apache.jena.sparql.util.Context; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.common.base.Preconditions; public class ServiceResponseCache { + private static final Logger logger = LoggerFactory.getLogger(ServiceResponseCache.class); + // Default parameters (can cache up to 150K bindings for 300 queries amounting to up to 45M bindings) public static final int DFT_MAX_ENTRY_COUNT = 300; public static final int DFT_PAGE_SIZE = 10000; @@ -52,33 +59,50 @@ public class ServiceResponseCache { /** Secondary index over cache keys */ protected Map idToKey = new ConcurrentHashMap<>(); + public record SimpleConfig(int maxCacheSize, int pageSize, int maxPageCount) {} + public ServiceResponseCache() { this(DFT_MAX_ENTRY_COUNT, DFT_PAGE_SIZE, DFT_MAX_PAGE_COUNT); } + public ServiceResponseCache(SimpleConfig config) { + this(config.maxCacheSize(), config.pageSize(), config.maxPageCount()); + } + public ServiceResponseCache(int maxCacheSize, int pageSize, int maxPageCount) { - //super(); - AsyncClaimingCacheImplGuava.Builder builder = - AsyncClaimingCacheImplGuava.newBuilder(CacheBuilder.newBuilder().maximumSize(maxCacheSize)); + this(maxCacheSize, () -> SliceInMemoryCache.create(ArrayOps.createFor(Binding.class), pageSize, maxPageCount)); + } + + public ServiceResponseCache(int maxCacheSize, Supplier> sliceFactory) { + super(); + AsyncClaimingCacheImplCaffeine.Builder builder = + AsyncClaimingCacheImplCaffeine.newBuilder(Caffeine.newBuilder().maximumSize(maxCacheSize)); builder = builder - .setCacheLoader(key -> { - long id = entryCounter.getAndIncrement(); - idToKey.put(id, key); - Slice slice = SliceInMemoryCache.create(ArrayOps.createFor(Binding.class), pageSize, maxPageCount); - ServiceCacheValue r = new ServiceCacheValue(id, slice); - Log.debug(ServiceResponseCache.class, "Loaded cache entry: " + id); - return r; - }) - .setAtomicRemovalListener(n -> { - // We are not yet handling cancellation of loading a key; in that case the value may not yet be available - // Handle it here here with null for v? - ServiceCacheValue v = n.getValue(); - if (v != null) { - long id = v.getId(); - Log.debug(ServiceResponseCache.class, "Removed cache entry: " + id); - idToKey.remove(id); + .setCacheLoader(key -> { + long id = entryCounter.getAndIncrement(); + idToKey.put(id, key); + Slice slice = sliceFactory.get(); + ServiceCacheValue r = new ServiceCacheValue(id, slice); + if (logger.isDebugEnabled()) { + logger.debug("Loaded cache entry: {} - {}", key.getServiceNode(), id); + } + return r; + }) + .setAtomicRemovalListener((k, v, c) -> { + // We are not yet handling cancellation of loading a key; in that case the value may not yet be available + // Handle it here here with null for v? + if (v != null) { + long id = v.getId(); + if (logger.isDebugEnabled()) { + logger.debug("Removed cache entry: {} - {}", k.getServiceNode(), id); } - }); + idToKey.remove(id); + } else { + if (logger.isDebugEnabled()) { + logger.debug("Removed cache entry without value {}", k); + } + } + }); cache = builder.build(); } @@ -104,10 +128,27 @@ public static ServiceResponseCache get() { } public static ServiceResponseCache get(Context cxt) { - return cxt.get(ServiceEnhancerConstants.serviceCache); + Lazy tmp = cxt.get(ServiceEnhancerConstants.serviceCache); + return tmp == null ? null : tmp.get(); } public static void set(Context cxt, ServiceResponseCache cache) { + cxt.put(ServiceEnhancerConstants.serviceCache, Lazy.ofInstance(cache)); + } + + public static void set(Context cxt, Lazy cache) { cxt.put(ServiceEnhancerConstants.serviceCache, cache); } + + public static ServiceResponseCache.SimpleConfig buildConfig(Context cxt) { + int maxEntryCount = cxt.getInt(ServiceEnhancerConstants.serviceCacheMaxEntryCount, ServiceResponseCache.DFT_MAX_ENTRY_COUNT); + int pageSize = cxt.getInt(ServiceEnhancerConstants.serviceCachePageSize, ServiceResponseCache.DFT_PAGE_SIZE); + int maxPageCount = cxt.getInt(ServiceEnhancerConstants.serviceCacheMaxPageCount, ServiceResponseCache.DFT_MAX_PAGE_COUNT); + + Preconditions.checkArgument(maxEntryCount > 0, ServiceEnhancerConstants.serviceCacheMaxEntryCount + " requires a value greater than 0"); + Preconditions.checkArgument(pageSize > 0, ServiceEnhancerConstants.serviceCachePageSize + " requires a value greater than 0"); + Preconditions.checkArgument(maxPageCount > 0, ServiceEnhancerConstants.serviceCacheMaxPageCount + " requires a value greater than 0"); + + return new ServiceResponseCache.SimpleConfig(maxEntryCount, pageSize, maxPageCount); + } } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceResultSizeCache.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceResultSizeCache.java index a4a0c82e04a..35fde1b923d 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceResultSizeCache.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/ServiceResultSizeCache.java @@ -21,13 +21,14 @@ package org.apache.jena.sparql.service.enhancer.impl; -import com.google.common.cache.Cache; -import com.google.common.cache.CacheBuilder; import org.apache.jena.graph.Node; import org.apache.jena.query.ARQ; import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerConstants; import org.apache.jena.sparql.util.Context; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; + /** * A mapping of service IRIs to result set size limits. * A flag indicates whether the limit is a lower bound or exact. diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/AssertionUtils.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/AssertionUtils.java new file mode 100644 index 00000000000..091d02f6e16 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/AssertionUtils.java @@ -0,0 +1,37 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util; + +public class AssertionUtils { + public static final boolean IS_ASSERT_ENABLED = isAssertEnabled(); + + public static boolean isAssertEnabled() { + boolean result; + try { + assert false; + result = false; + } catch (AssertionError e) { + result = true; + } + return result; + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/AutoCloseableBase.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/AutoCloseableBase.java index d6a4f5d19ba..1a88401765b 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/AutoCloseableBase.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/AutoCloseableBase.java @@ -26,33 +26,47 @@ public class AutoCloseableBase { protected volatile boolean isClosed = false; - /** - * To be called within synchronized functions - */ + protected boolean enableCloseStackTrace; + protected StackTraceElement[] closeStackTrace = null; + + public AutoCloseableBase() { + this(true); + } + + public AutoCloseableBase(boolean enableCloseStackTrace) { + this.enableCloseStackTrace = enableCloseStackTrace; + } + + /** To be called within synchronized functions */ protected void ensureOpen() { if (isClosed) { - throw new RuntimeException("Object already closed"); + String str = StackTraceUtils.toString(closeStackTrace); + throwClosedException("Object already closed at: " + str); } } - protected void closeActual() throws Exception { - // Nothing to do here; override if needed + protected void throwClosedException(String msg) { + throw new RuntimeException(msg); } + protected void closeActual() throws Exception { /* nothing to do */ } + @Override public final void close() { if (!isClosed) { synchronized (this) { if (!isClosed) { - isClosed = true; - + closeStackTrace = enableCloseStackTrace ? StackTraceUtils.getStackTraceIfEnabled() : null; try { closeActual(); } catch (Exception e) { throw new RuntimeException(e); + } finally { + isClosed = true; } } } } } } + diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/AutoCloseableWithLeakDetectionBase.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/AutoCloseableWithLeakDetectionBase.java index 47b151a8219..c7fac4dd219 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/AutoCloseableWithLeakDetectionBase.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/AutoCloseableWithLeakDetectionBase.java @@ -21,6 +21,7 @@ package org.apache.jena.sparql.service.enhancer.impl.util; +import org.apache.commons.lang3.ObjectUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,29 +31,40 @@ * If finalize is called (typically only by the GC) and there was no prior call to close then * a warning including the stack trace is logged. * - * Implementing classes should override {@link #closeActual()} rather than - * {@link #close()}. + * Implementing classes need to override {@link #closeActual()} because {@link #close()} is final. */ public class AutoCloseableWithLeakDetectionBase extends AutoCloseableBase { private static final Logger logger = LoggerFactory.getLogger(AutoCloseableWithLeakDetectionBase.class); - protected final StackTraceElement[] instantiationStackTrace = StackTraceUtils.getStackTraceIfEnabled(); + protected final StackTraceElement[] instantiationStackTrace; + + public AutoCloseableWithLeakDetectionBase() { + this(true); + } + + public AutoCloseableWithLeakDetectionBase(boolean enableInstantiationStackTrace) { + super(enableInstantiationStackTrace); + this.instantiationStackTrace = enableInstantiationStackTrace + ? StackTraceUtils.getStackTraceIfEnabled() + : null; + } public StackTraceElement[] getInstantiationStackTrace() { return instantiationStackTrace; } - @SuppressWarnings("removal") @Override + @SuppressWarnings("removal") protected void finalize() throws Throwable { try { if (!isClosed) { - String str = StackTraceUtils.toString(instantiationStackTrace); - - logger.warn("Close invoked via GC rather than user logic - indicates resource leak. Object constructed at " + str); - + if (logger.isWarnEnabled()) { + String objectIdStr = ObjectUtils.identityToString(this); + String stackTraceStr = StackTraceUtils.toString(instantiationStackTrace); + logger.warn(String.format("Close invoked via GC rather than user logic - indicates resource leak. Object %s constructed at %s", objectIdStr, stackTraceStr)); + } close(); } } finally { diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/BindingUtils.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/BindingUtils.java index a0d7ba023a8..91a672fefe6 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/BindingUtils.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/BindingUtils.java @@ -101,6 +101,7 @@ public static Set varsMentioned(Binding binding) { * If the node value is null then null is returned. * If the node value is not a number literal then an {@link ExprEvalException} is raised. */ public static Number getNumberOrNull(Binding binding, Var var) { + Objects.requireNonNull(binding); Node node = binding.get(var); Number result = NodeUtilsExtra.getNumberOrNull(node); return result; diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/FinallyRunAll.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/FinallyRunAll.java index 8c3b8d741d2..0bfb15ee29e 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/FinallyRunAll.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/FinallyRunAll.java @@ -31,7 +31,7 @@ * * Usage: *
{@code
- * FinallyAll.run(
+ * FinallyRunAll.run(
  *   () -> action1(),
  *   () -> action2(),
  *   () -> actionN()
diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/Disposable.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/InstanceLifeCycle.java
similarity index 79%
rename from jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/Disposable.java
rename to jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/InstanceLifeCycle.java
index 513f800d5bf..6caf20e0f1a 100644
--- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/Disposable.java
+++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/InstanceLifeCycle.java
@@ -19,12 +19,9 @@
  *   SPDX-License-Identifier: Apache-2.0
  */
 
-package org.apache.jena.sparql.service.enhancer.slice.api;
+package org.apache.jena.sparql.service.enhancer.impl.util;
 
-/** Interface typically used for removing listener registrations */
-public interface Disposable
-    extends AutoCloseable
-{
-    @Override
-    void close();
+public interface InstanceLifeCycle {
+    T newInstance();
+    void closeInstance(T inst);
 }
diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/InstanceLifeCycles.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/InstanceLifeCycles.java
new file mode 100644
index 00000000000..d122a507615
--- /dev/null
+++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/InstanceLifeCycles.java
@@ -0,0 +1,99 @@
+/*
+ * 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
+ *
+ *   https://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.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.apache.jena.sparql.service.enhancer.impl.util;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public class InstanceLifeCycles {
+    public static class InstanceLifeCycleImpl
+        implements InstanceLifeCycle, Serializable
+    {
+        private static final long serialVersionUID = 1L;
+        protected Supplier creator;
+        protected Consumer closer;
+
+        public InstanceLifeCycleImpl(Supplier creator, Consumer closer) {
+            this.creator = creator;
+            this.closer = closer;
+        }
+
+        @Override
+        public T newInstance() {
+            return this.creator.get();
+        }
+
+        @Override
+        public void closeInstance(T inst) {
+            closer.accept(inst);
+        }
+    }
+
+    public static  InstanceLifeCycle of(Supplier creator, Consumer closer) {
+        return new InstanceLifeCycleImpl<>(creator, closer);
+    }
+
+    public static  InstanceLifeCycle enclose(InstanceLifeCycle lifeCycle, Runnable beforeAction, Runnable afterAction) {
+        return of(() -> {
+            if (beforeAction != null) {
+                beforeAction.run();
+            }
+            T r = lifeCycle.newInstance();
+            return r;
+        }, inst -> {
+            try {
+                lifeCycle.closeInstance(inst);
+            } finally {
+                if (afterAction != null) {
+                    afterAction.run();
+                }
+            }
+        });
+    }
+
+    public static  InstanceLifeCycle> enclose(InstanceLifeCycle outer, InstanceLifeCycle inner) {
+        return of(() -> {
+            O o = outer.newInstance();
+            I i;
+            try {
+                i = inner.newInstance();
+            } catch (Exception e) {
+                // On error creating the inner instance close the outer one
+                outer.closeInstance(o);
+                throw new RuntimeException(e);
+            }
+            return Map.entry(o, i);
+        },
+        e -> {
+            O o = e.getKey();
+            I i = e.getValue();
+            try {
+                outer.closeInstance(o);
+            } finally {
+                inner.closeInstance(i);
+            }
+        });
+    }
+}
diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/IteratorUtils.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/IteratorUtils.java
index 992e0142ed0..9e9ee4fffc8 100644
--- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/IteratorUtils.java
+++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/IteratorUtils.java
@@ -32,6 +32,7 @@
 import java.util.function.Function;
 
 import org.apache.jena.atlas.iterator.Iter;
+
 import com.google.common.collect.AbstractIterator;
 import com.google.common.collect.Table.Cell;
 import com.google.common.collect.Tables;
diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/Lazy.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/Lazy.java
new file mode 100644
index 00000000000..3f79afdddff
--- /dev/null
+++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/Lazy.java
@@ -0,0 +1,60 @@
+/*
+ * 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
+ *
+ *   https://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.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.apache.jena.sparql.service.enhancer.impl.util;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/** Wrapper to initialize an instance lazily. */
+public class Lazy {
+    private Supplier initializer;
+    private volatile T instance;
+
+    private Lazy(Supplier initializer, T instance) {
+        super();
+        this.initializer = initializer;
+        this.instance = instance;
+    }
+
+    public static  Lazy of(Supplier initializer) {
+        return new Lazy<>(Objects.requireNonNull(initializer), null);
+    }
+
+    public static  Lazy ofInstance(T instance) {
+        return new Lazy<>(null, Objects.requireNonNull(instance));
+    }
+
+    public boolean isSet() {
+        return instance != null;
+    }
+
+    public T get() {
+        if (instance == null) {
+            synchronized (this) {
+                if (instance == null) {
+                    instance = initializer.get();
+                }
+            }
+        }
+        return instance;
+    }
+}
diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/LockUtils.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/LockUtils.java
deleted file mode 100644
index 897e4bdb476..00000000000
--- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/LockUtils.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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
- *
- *   https://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.
- *
- *   SPDX-License-Identifier: Apache-2.0
- */
-
-package org.apache.jena.sparql.service.enhancer.impl.util;
-
-import java.time.Duration;
-import java.util.concurrent.Callable;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.locks.Lock;
-import java.util.function.Consumer;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public class LockUtils {
-    private static final Logger logger = LoggerFactory.getLogger(LockUtils.class);
-
-    /**
-     * Perform an action which requires acquisition of a lock first.
-     * An attempt is made to acquire the lock. If this fails then the action is not run.
-     * Upon completion of the action (successful or exceptional) the lock is released again.
-     */
-    public static  T runWithLock(Lock lock, Callable action) {
-        T result = null;
-        try {
-            lock.lock();
-            result = action.call();
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        } finally {
-            lock.unlock();
-        }
-        return result;
-    }
-
-    /** Run an action after locking; eventually the lock is unlocked in a finally block */
-    public static void runWithLock(Lock lock, ThrowingRunnable action) {
-        runWithLock(lock, () -> { action.run(); return null; });
-    }
-
-    /**
-     * Run this action with a short-lived locked. If the lock cannot be acquired
-     * within the given time it is considered stale and forcibly unlocked.
-     * Subsequently another attempt is made to acquire the lock.
-     */
-    public static  T runWithMgmtLock(
-            L lock,
-            Consumer forceUnlock,
-            Duration duration,
-            Callable action) {
-        T result = null;
-        try {
-            long timeout = duration.toMillis();
-            boolean isLocked;
-            if (!(isLocked = lock.tryLock(timeout, TimeUnit.MILLISECONDS))) {
-
-                logger.warn(String.format("Forcibly unlocking stale lock %s", lock));
-                forceUnlock.accept(lock);
-
-                isLocked = lock.tryLock(timeout, TimeUnit.MILLISECONDS);
-                if (!isLocked) {
-                    throw new RuntimeException("Failed to acquire lock despite forced unlocking");
-                }
-            }
-
-            result = action.call();
-        } catch (Exception e) {
-            throw new RuntimeException(e);
-        } finally {
-            lock.unlock();
-        }
-        return result;
-    }
-
-}
diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/PageUtils.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/PageUtils.java
index 42cc0d6df90..c83e90b8f46 100644
--- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/PageUtils.java
+++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/PageUtils.java
@@ -22,14 +22,13 @@
 package org.apache.jena.sparql.service.enhancer.impl.util;
 
 import java.util.Collection;
-import java.util.NavigableSet;
-import java.util.TreeSet;
-import java.util.stream.Collectors;
 import java.util.stream.LongStream;
 
 import com.google.common.collect.ContiguousSet;
 import com.google.common.collect.DiscreteDomain;
 import com.google.common.collect.Range;
+import com.google.common.collect.RangeSet;
+import com.google.common.collect.TreeRangeSet;
 
 /**
  * Utility methods for working with (fixed-size) pages.
@@ -51,6 +50,29 @@ public static long getPageOffsetForId(long pageId, long pageSize) {
         return pageId * pageSize;
     }
 
+    /**
+     * Convert a range in item-offset space to page-id space.
+     * For example, the offset range [1000,2000) with a page size of 1000 will become the page index range [1,2).
+     */
+    public static Range touchedPageIndexRange(Range range, long pageSize) {
+        DiscreteDomain discreteDomain = DiscreteDomain.longs();
+        ContiguousSet set = ContiguousSet.create(range, discreteDomain);
+        long start = getPageIndexForOffset(set.first(), pageSize);
+        long end = getPageIndexForOffset(set.last(), pageSize);
+        Range rawRange = Range.closed(start, end);
+        Range canonicalRange = rawRange.canonical(discreteDomain);
+        return canonicalRange;
+    }
+
+    /**
+     * Similar to {@link #touchedPageIndexRange(Range, long)} but for a set of ranges.
+     */
+    public static RangeSet touchedPageIndexRangeSet(Collection> offsetRanges, long pageSize) {
+        RangeSet result = TreeRangeSet.create();
+        offsetRanges.forEach(offsetRange -> result.add(touchedPageIndexRange(offsetRange, pageSize)));
+        return result;
+    }
+
     /** Return a stream of the page indices touched by the range w.r.t. the page size */
     public static LongStream touchedPageIndices(Range range, long pageSize) {
         ContiguousSet set = ContiguousSet.create(range, DiscreteDomain.longs());
@@ -61,14 +83,4 @@ public static LongStream touchedPageIndices(Range range, long pageSize) {
                         getPageIndexForOffset(set.last(), pageSize));
         return result;
     }
-
-    public static NavigableSet touchedPageIndices(Collection> ranges, long pageSize) {
-        NavigableSet result = ranges.stream()
-            .flatMapToLong(range -> PageUtils.touchedPageIndices(range, pageSize))
-            .boxed()
-            .collect(Collectors.toCollection(TreeSet::new));
-
-        return result;
-    }
-
 }
diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/PeekIteratorLazy.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/PeekIteratorLazy.java
index fedb41e779a..318131909fb 100644
--- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/PeekIteratorLazy.java
+++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/PeekIteratorLazy.java
@@ -24,10 +24,15 @@
 import java.util.Iterator;
 import java.util.Objects;
 
+import org.apache.jena.atlas.iterator.PeekIterator;
+
 import com.google.common.collect.AbstractIterator;
 import com.google.common.collect.PeekingIterator;
 
-/** The atlas version does active read ahead; this one only fetches data when needed */
+/**
+ * An iterator with support for peeking the next item.
+ * The {@link PeekIterator} in jena-atlas uses active read ahead whereas this class only fetches data when needed.
+ */
 public class PeekIteratorLazy
     extends AbstractIterator // AbstractIterator already has a public peek method
     implements PeekingIterator
diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/QueryIterDefer.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/QueryIterDefer.java
index 1293e8c91f2..d941960dcc9 100644
--- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/QueryIterDefer.java
+++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/QueryIterDefer.java
@@ -24,20 +24,20 @@
 import java.util.Objects;
 import java.util.function.Supplier;
 
-import org.apache.jena.atlas.io.IndentedWriter;
+import org.apache.jena.sparql.engine.ExecutionContext;
 import org.apache.jena.sparql.engine.QueryIterator;
 import org.apache.jena.sparql.engine.binding.Binding;
-import org.apache.jena.sparql.engine.iterator.QueryIteratorWrapper;
-import org.apache.jena.sparql.serializer.SerializationContext;
+import org.apache.jena.sparql.engine.iterator.QueryIter;
 
 /** Deferred (lazy) iterator which initializes a delegate from a supplier only when needed */
 public class QueryIterDefer
-    extends QueryIteratorWrapper
+    extends QueryIter
 {
     protected Supplier supplier;
+    protected QueryIterator iterator;
 
-    public QueryIterDefer(Supplier supplier) {
-        super(null);
+    public QueryIterDefer(ExecutionContext execCxt, Supplier supplier) {
+        super(execCxt);
         this.supplier = supplier;
     }
 
@@ -50,29 +50,26 @@ protected void ensureInitialized() {
     @Override
     protected boolean hasNextBinding() {
         ensureInitialized();
-        return super.hasNextBinding();
+        return iterator.hasNext();
     }
 
     @Override
     protected Binding moveToNextBinding() {
         ensureInitialized();
-        return super.moveToNextBinding();
+        return iterator.hasNext() ? iterator.next() : null;
     }
 
     @Override
-    public void output(IndentedWriter out) {
-        ensureInitialized();
-        super.output(out);
+    protected void requestCancel() {
+        if (iterator != null) {
+            iterator.cancel();
+        }
     }
 
     @Override
     protected void closeIterator() {
-        super.closeIterator();
-    }
-
-    @Override
-    public void output(IndentedWriter out, SerializationContext sCxt) {
-        ensureInitialized();
-        super.output(out, sCxt);
+        if (iterator != null) {
+            iterator.close();
+        }
     }
 }
diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/QueryIterSlottedBase.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/QueryIterSlottedBase.java
index 94c5ede635c..bb54c606e1d 100644
--- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/QueryIterSlottedBase.java
+++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/QueryIterSlottedBase.java
@@ -21,70 +21,83 @@
 
 package org.apache.jena.sparql.service.enhancer.impl.util;
 
-import org.apache.jena.atlas.io.IndentedWriter;
-import org.apache.jena.atlas.io.Printable;
-import org.apache.jena.atlas.iterator.IteratorSlotted;
+import java.util.NoSuchElementException;
+
 import org.apache.jena.atlas.lib.Lib;
-import org.apache.jena.shared.PrefixMapping;
-import org.apache.jena.sparql.engine.Plan;
-import org.apache.jena.sparql.engine.QueryIterator;
+import org.apache.jena.sparql.engine.ExecutionContext;
 import org.apache.jena.sparql.engine.binding.Binding;
-import org.apache.jena.sparql.serializer.SerializationContext;
-import org.apache.jena.sparql.util.QueryOutputUtils;
+import org.apache.jena.sparql.engine.iterator.QueryIter;
 
 /**
- * QueryIterator implementation based on IteratorSlotted.
- * Its purpose is to ease wrapping a non-QueryIterator as one based
- * on a {@link IteratorSlotted#moveToNext} method analogous
- * to guava's AbstractIterator.
+ * Abstract implementation of {@link QueryIter} that delegates computation of the next element to {@link QueryIterSlottedBase#moveToNext()}.
  */
 public abstract class QueryIterSlottedBase
-    extends IteratorSlotted
-    implements QueryIterator
+    extends QueryIter
 {
-    @Override
-    public Binding nextBinding() {
-        Binding result = isFinished()
-                ? null
-                : next();
-        return result;
-    }
+    private boolean slotIsSet = false;
+    private boolean hasMore = true;
+    private Binding slot = null;
 
-    @Override
-    protected boolean hasMore() {
-        return !isFinished();
+    public QueryIterSlottedBase(ExecutionContext execCxt) {
+        super(execCxt);
     }
 
     @Override
-    public String toString(PrefixMapping pmap)
-    { return QueryOutputUtils.toString(this, pmap); }
+    protected final boolean hasNextBinding() {
+        if ( slotIsSet )
+            return true;
 
-    // final stops it being overridden and missing the output() route.
-    @Override
-    public final String toString()
-    { return Printable.toString(this); }
+        if (!hasMore) {
+            return false;
+        }
+    //    boolean r = hasMore();
+    //    if ( !r ) {
+    //        close();
+    //        return false;
+    //    }
 
-    /** Normally overridden for better information */
-    @Override
-    public void output(IndentedWriter out)
-    {
-        out.print(Plan.startMarker);
-        out.print(Lib.className(this));
-        out.print(Plan.finishMarker);
+        slot = moveToNext();
+        // if ( slot == null ) {
+        if (!hasMore) {
+            close();
+            return false;
+        }
+
+        slotIsSet = true;
+        return true;
+    }
+
+    protected final Binding endOfData() {
+        hasMore = false;
+        return null;
     }
 
     @Override
-    public void cancel() {
-        close();
+    public final Binding moveToNextBinding() {
+        if ( !hasNext() )
+            throw new NoSuchElementException(Lib.className(this));
+
+        Binding obj = slot;
+        slot = null;
+        slotIsSet = false;
+        return obj;
     }
 
     @Override
-    public void output(IndentedWriter out, SerializationContext sCxt) {
-        output(out);
-//	        out.println(Lib.className(this) + "/" + Lib.className(iterator));
-//	        out.incIndent();
-//	        // iterator.output(out, sCxt);
-//	        out.decIndent();
-//	        // out.println(Utils.className(this)+"/"+Utils.className(iterator));
+    protected final void closeIterator() {
+        // Called by QueryIterBase.close()
+        slotIsSet = false;
+        slot = null;
+
+        closeIteratorActual();
     }
+
+    protected abstract void closeIteratorActual();
+
+    /**
+     * Method that must return the next non-null element.
+     * A return value of null indicates that the iterator's end has been reached.
+     */
+    protected abstract Binding moveToNext();
+
 }
diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/RangeUtils.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/RangeUtils.java
index eac82c92410..3e6dec5a775 100644
--- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/RangeUtils.java
+++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/RangeUtils.java
@@ -23,11 +23,11 @@
 
 import java.util.function.Function;
 
+import org.apache.jena.query.Query;
+
 import com.google.common.collect.Range;
 import com.google.common.collect.RangeSet;
 
-import org.apache.jena.query.Query;
-
 /** Utility methods for working with guava {@link Range} instances */
 public class RangeUtils {
     public static > RangeSet gaps(Range requestRange, RangeSet availableRanges) {
diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/SinglePrefetchIterator.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/SinglePrefetchIterator.java
index 1de98be26ac..c1d9da3dd0e 100644
--- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/SinglePrefetchIterator.java
+++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/SinglePrefetchIterator.java
@@ -68,10 +68,14 @@ private void _prefetch()
         }
         catch(Exception e) {
             current = null;
-            throw new RuntimeException("Prefetching data failed. Reason: " + e.getMessage(), e);
+            handleException(e);
         }
     }
 
+    protected void handleException(Throwable e) {
+        throw new RuntimeException("Prefetching data failed. Reason: " + e.getMessage(), e);
+    }
+
     @Override
     public boolean hasNext()
     {
diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/StackTraceUtils.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/StackTraceUtils.java
index cccdec3d131..5b67a090487 100644
--- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/StackTraceUtils.java
+++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/StackTraceUtils.java
@@ -26,29 +26,13 @@
 import java.util.stream.Collectors;
 
 public class StackTraceUtils {
-
-    public static final boolean IS_ASSERT_ENABLED = isAssertEnabled();
-
-    public static boolean isAssertEnabled() {
-        boolean result;
-        try {
-           assert false;
-           result = false;
-        } catch (AssertionError e) {
-           result = true;
-        }
-        return result;
-    }
-
     public static StackTraceElement[] getStackTraceIfEnabled() {
-        StackTraceElement[] result = IS_ASSERT_ENABLED
+        StackTraceElement[] result = AssertionUtils.IS_ASSERT_ENABLED
                 ? Thread.currentThread().getStackTrace()
                 : null;
-
         return result;
     }
 
-
     public static String toString(StackTraceElement[] stackTrace) {
         String result = stackTrace == null
                 ? "(stack traces not enabled - enable assertions using the -ea jvm option)"
@@ -57,5 +41,4 @@ public static String toString(StackTraceElement[] stackTrace) {
 
         return result;
     }
-
 }
diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/VarScopeUtils.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/VarScopeUtils.java
index 704b0bf4912..53ca3225004 100644
--- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/VarScopeUtils.java
+++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/VarScopeUtils.java
@@ -21,16 +21,22 @@
 
 package org.apache.jena.sparql.service.enhancer.impl.util;
 
-import java.util.*;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.Set;
+import java.util.TreeSet;
 import java.util.stream.Collectors;
 
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
-
 import org.apache.jena.sparql.ARQConstants;
 import org.apache.jena.sparql.core.Var;
 import org.apache.jena.sparql.engine.Rename;
 
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+
 /**
  * Methods for working with scope levels of SPARQL variables.
  * Includes methods for getting, setting and normalizing scope levels.
@@ -47,6 +53,14 @@ public static String getPlainName(String varName) {
         return result;
     }
 
+    public static Set getPlainNames(Collection vars) {
+        Set result = vars.stream()
+                .map(Var::getName)
+                .map(VarScopeUtils::getPlainName)
+                .collect(Collectors.toCollection(LinkedHashSet::new));
+        return result;
+    }
+
     public static int getScopeLevel(Var var) {
         return getScopeLevel(var.getName());
     }
@@ -99,32 +113,54 @@ public static Map getMinimumScopeLevels(Collection vars) {
         return result;
     }
 
+    /**
+     * Map each plain variable name to the set of scope levels (ordered ascending).
+     */
+    public static Map> getScopeLevels(Collection vars) {
+        Map> result = new LinkedHashMap<>();
+        for (Var var : vars) {
+            String scopedName = var.getName();
+            String plainName = getPlainName(scopedName);
+            int level = getScopeLevel(scopedName);
+
+            NavigableSet levels = result.computeIfAbsent(plainName, key -> new TreeSet<>());
+            levels.add(level);
+        }
+        return result;
+    }
+
     /**
      * Return a mapping that reduces every variable's scope level by the minimum scope level
      * among the variables having the same base name.
-     * Consequently, for every variable name the minimum scope level will be normalized to 0.
+     * Consequently, the scope of every visible variable will be normalized to 0; for non-visible ones the scope becomes 1.
      * 

- * Example: normalizeVarScopes({?a, ?/b, ?//c, ?////c}) yields: + * Example: normalizeVarScopes({?a, ?/b, ?//c, ?////c}, visible={a, c}) yields: *

    *
  • ?a -> ?a
  • - *
  • ?/b -> ?b
  • + *
  • ?//b -> ?/b (normalized to 1 because b is not visible)
  • *
  • ?//c -> ?c
  • *
  • ?////c -> ?//c
  • *
* - * @param vars A set of variables with arbitrary scope levels. - * @return A mapping that normalizes every variable's minimum scope to 0. + * @param mentionedScopedVars A set of variables with arbitrary scope levels. + * @return A mapping that normalizes every variable's minimum scope to either 0 or 1 depending on visibility. */ - public static BiMap normalizeVarScopes(Collection vars) { - Map nameToMinLevel = getMinimumScopeLevels(vars); + public static BiMap normalizeVarScopes(Collection mentionedScopedVars, Set visibleUnscopedVarName) { + Map nameToMinLevel = getMinimumScopeLevels(mentionedScopedVars); BiMap result = HashBiMap.create(); - for (Var from : vars) { + for (Var from : mentionedScopedVars) { String fromName = from.getName(); int fromLevel = getScopeLevel(fromName); String plainName = getPlainName(fromName); int minLevel = nameToMinLevel.get(plainName); int normalizedLevel = fromLevel - minLevel; + + // Increase the scope level by one for non-visible vars + if (!visibleUnscopedVarName.contains(plainName)) { + ++normalizedLevel; + } + Var to = allocScoped(plainName, normalizedLevel); result.put(from, to); } @@ -132,7 +168,7 @@ public static BiMap normalizeVarScopes(Collection vars) { } /** - * Similar to {@link #normalizeVarScopes(Collection)}, however reduces the scope levels of all variables + * Similar to {@link #normalizeVarScopes(Collection, Set)}, however reduces the scope levels of all variables * by the globally minimum scope level. * In other words, if the minimum scope level among all given variables is 'n' then the returned mapping * reduces every scope level by 'n'. diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIterator.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIterator.java new file mode 100644 index 00000000000..2c9612d2f6c --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIterator.java @@ -0,0 +1,36 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util.iterator; + +import org.apache.jena.atlas.iterator.IteratorCloseable; +import org.apache.jena.sparql.util.PrintSerializable; + +public interface AbortableIterator extends IteratorCloseable, PrintSerializable +{ + /** Get next binding */ + public T nextBinding() ; + + /** + * Cancels the query as soon as is possible for the given iterator + */ + public void cancel(); +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIterator1.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIterator1.java new file mode 100644 index 00000000000..1513df26bf1 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIterator1.java @@ -0,0 +1,92 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util.iterator; + +import org.apache.jena.atlas.io.IndentedWriter; +import org.apache.jena.atlas.lib.Lib; +import org.apache.jena.sparql.serializer.SerializationContext; + +/** Base class for abortable iterators that are based on an input one. */ +public abstract class AbortableIterator1 + extends AbortableIteratorBase +{ + private AbortableIterator input; + + public AbortableIterator1(AbortableIterator input) { + super(); + this.input = input; + } + + protected AbortableIterator getInput() { return input ; } + + @Override + protected final void closeIterator() { + closeSubIterator(); + performClose(input); + input = null; + } + + @Override + protected final void requestCancel() { + requestSubCancel(); + performRequestCancel(input); + } + + /** Cancellation of the query execution is happening */ + protected abstract void requestSubCancel(); + + /** + * Pass on the close method - no need to close the QueryIterator passed to the + * QueryIter1 constructor + */ + protected abstract void closeSubIterator(); + + @Override + public void output(IndentedWriter out) { + output(out, null); + } + + // Do better + @Override + public void output(IndentedWriter out, SerializationContext sCxt) { + // Linear form. + if ( getInput() != null ) + // Closed + getInput().output(out, sCxt); + else + out.println("Closed"); + out.ensureStartOfLine(); + details(out, sCxt); + out.ensureStartOfLine(); + +// details(out, sCxt) ; +// out.ensureStartOfLine() ; +// out.incIndent() ; +// getInput().output(out, sCxt) ; +// out.decIndent() ; +// out.ensureStartOfLine() ; + } + + protected void details(IndentedWriter out, SerializationContext sCxt) { + out.println(Lib.className(this)); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorBase.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorBase.java new file mode 100644 index 00000000000..8abcd7152df --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorBase.java @@ -0,0 +1,247 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util.iterator; + +import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.jena.atlas.io.IndentedWriter; +import org.apache.jena.atlas.io.Printable; +import org.apache.jena.atlas.lib.Lib; +import org.apache.jena.atlas.logging.Log; +import org.apache.jena.query.QueryCancelledException; +import org.apache.jena.query.QueryException; +import org.apache.jena.query.QueryFatalException; +import org.apache.jena.shared.PrefixMapping; +import org.apache.jena.sparql.engine.Plan; +import org.apache.jena.sparql.service.enhancer.impl.util.AutoCloseableWithLeakDetectionBase; +import org.apache.jena.sparql.util.QueryOutputUtils; + +/** This is a generalization of QueryIterator to a generic type T. */ +public abstract class AbortableIteratorBase + extends AutoCloseableWithLeakDetectionBase + implements AbortableIterator +{ + private boolean finished = false; + + // === Cancellation + + // .cancel() can be called asynchronously with iterator execution. + // It causes notification to cancellation to be made, once, by calling .requestCancel() + // which is called synchronously with .cancel() and asynchronously with iterator execution. + private final AtomicBoolean requestingCancel; + private volatile boolean cancelOnce = false; + private Object cancelLock = new Object(); + + /** QueryIteratorBase with no cancellation facility */ + protected AbortableIteratorBase() { + // No async cancellation. + this(null); + } + + /** Argument : shared flag for cancellation. */ + protected AbortableIteratorBase(AtomicBoolean cancelSignal) { + super(true); + requestingCancel = (cancelSignal == null) + ? new AtomicBoolean(false) // Allows for direct cancel (not timeout). + : cancelSignal; + } + + private boolean requestingCancel() { + return (requestingCancel != null && requestingCancel.get()) || Thread.interrupted() ; + } + + // -------- The contract with the subclasses + + /** + * Implement this, not hasNext(). + * Do not throw {@link NoSuchElementException}. + */ + protected abstract boolean hasNextBinding(); + + /** + * Implement this, not next() or nextBinding(). + * Returning null is turned into + * NoSuchElementException. Does not need to call hasNextBinding. + */ + protected abstract T moveToNextBinding(); + + /** Close the iterator. */ + protected abstract void closeIterator(); + + /** Propagates the cancellation request - called asynchronously with the iterator itself */ + protected abstract void requestCancel(); + + /* package */ boolean getRequestingCancel() { + return requestingCancel(); + } + + // -------- The contract with the subclasses + + protected boolean isFinished() { return finished; } + + /** final - subclasses implement hasNextBinding() */ + @Override + public final boolean hasNext() { + if ( finished ) + // Even if aborted. Finished is finished. + return false; + + if ( cancelOnce ) { + // Try to close first to release resources (in case the user + // doesn't have a close() call in a finally block) + close(); + throw new QueryCancelledException(); + } + + // Handles exceptions + boolean r = hasNextBinding(); + + if ( r == false ) + try { + close(); + } catch (QueryFatalException ex) { + Log.error(this, "Fatal exception: " + ex.getMessage()); + throw ex; // And pass on up the exception. + } + return r; + } + + /** + * final - autoclose and registration relies on it - implement + * moveToNextBinding() + */ + @Override + public final T next() { + return nextBinding(); + } + + /** final - subclasses implement moveToNextBinding() */ + @Override + public final T nextBinding() { + try { + // Need to make sure to only read this once per iteration + boolean shouldCancel = requestingCancel(); + + if ( shouldCancel ) { + // Try to close first to release resources (in case the user + // doesn't have a close() call in a finally block) + close(); + throw new QueryCancelledException(); + } + + if ( finished ) + throw new NoSuchElementException(Lib.className(this)); + + if ( !hasNextBinding() ) + throw new NoSuchElementException(Lib.className(this)); + + T obj = moveToNextBinding(); + if ( obj == null ) + throw new NoSuchElementException(Lib.className(this)); + + if ( shouldCancel && !finished ) { + // But .cancel sets both requestingCancel and abortIterator + // This only happens with a continuing iterator. + close(); + } + + return obj; + } catch (QueryFatalException ex) { + Log.error(this, "QueryFatalException", ex); + throw ex; + } + } + + @Override + public final void remove() { + Log.warn(this, "Call to QueryIterator.remove() : " + Lib.className(this) + ".remove"); + throw new UnsupportedOperationException(Lib.className(this) + ".remove"); + } + + @Override + protected final void closeActual() { + if ( finished ) + return; + try { + closeIterator(); + } catch (QueryException ex) { + Log.warn(this, "QueryException in close()", ex); + } + finished = true; + } + + /** Cancel this iterator : this is called, possibly asynchronously, to cancel an iterator.*/ + @Override + public final void cancel() { + synchronized (cancelLock) { + if ( ! cancelOnce ) { + // Need to set the flags before allowing subclasses to handle requestCancel() in order + // to prevent a race condition. We want to be sure that calls to have hasNext()/nextBinding() + // will definitely throw a QueryCancelledException in this class and + // not allow a situation in which a subclass component thinks it is cancelled, + // while this class does not. + if ( requestingCancel != null ) + // Signalling from timeouts + requestingCancel.set(true); + cancelOnce = true; + this.requestCancel(); + } + } + } + + /** close an iterator */ + protected static void performClose(AbortableIterator iter) { + if ( iter == null ) + return; + iter.close(); + } + + /** + * Cancel an iterator. Best-effort for non-blocking concurrent cancel because + * iter may longer be in-use when cancel() is called on it. + */ + protected static void performRequestCancel(AbortableIterator iter) { + if ( iter == null ) + return; + iter.cancel(); + } + + // --- PrintSerializableBase --- + + @Override + public String toString(PrefixMapping pmap) + { return QueryOutputUtils.toString(this, pmap) ; } + + // final stops it being overridden and missing the output() route. + @Override + public final String toString() + { return Printable.toString(this) ; } + + /** Normally overridden for better information */ + @Override + public void output(IndentedWriter out) { + out.print(Plan.startMarker); + out.print(Lib.className(this)); + out.print(Plan.finishMarker); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorConcat.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorConcat.java new file mode 100644 index 00000000000..07367b4c679 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorConcat.java @@ -0,0 +1,143 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util.iterator; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.apache.jena.atlas.io.IndentedWriter; +import org.apache.jena.atlas.lib.Lib; +import org.apache.jena.sparql.engine.binding.Binding; +import org.apache.jena.sparql.serializer.SerializationContext; + +/** + * A query iterator that joins two or more iterators into a single iterator. */ + +public class AbortableIteratorConcat extends AbortableIteratorBase +{ + boolean initialized = false ; + List> iteratorList = new ArrayList<>() ; + Iterator> iterator ; + AbortableIterator currentQIter = null ; + + Binding binding ; + boolean doneFirst = false ; + + public AbortableIteratorConcat() + { + super() ; + } + + private void init() + { + if ( ! initialized ) + { + currentQIter = null ; + if ( iterator == null ) + iterator = iteratorList.listIterator() ; + if ( iterator.hasNext() ) + currentQIter = iterator.next() ; + initialized = true ; + } + } + + public void add(AbortableIterator qIter) + { + if ( qIter != null ) + iteratorList.add(qIter) ; + } + + + @Override + protected boolean hasNextBinding() + { + if ( isFinished() ) + return false ; + + init() ; + if ( currentQIter == null ) + return false ; + + while ( ! currentQIter.hasNext() ) + { + // End sub iterator + //currentQIter.close() ; + currentQIter = null ; + if ( iterator.hasNext() ) + currentQIter = iterator.next() ; + if ( currentQIter == null ) + { + // No more. + //close() ; + return false ; + } + } + + return true ; + } + + @Override + protected T moveToNextBinding() + { + if ( ! hasNextBinding() ) + throw new NoSuchElementException(Lib.className(this)) ; + if ( currentQIter == null ) + throw new NoSuchElementException(Lib.className(this)) ; + + T binding = currentQIter.nextBinding() ; + return binding ; + } + + + @Override + protected void closeIterator() + { + for ( AbortableIterator qIter : iteratorList ) + { + performClose( qIter ); + } + } + + @Override + protected void requestCancel() + { + for ( AbortableIterator qIter : iteratorList ) + { + performRequestCancel( qIter ); + } + } + + @Override + public void output(IndentedWriter out, SerializationContext sCxt) + { + out.println(Lib.className(this)) ; + out.incIndent() ; + for ( AbortableIterator qIter : iteratorList ) + { + qIter.output( out, sCxt ); + } + out.decIndent() ; + out.ensureStartOfLine() ; + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorConvert.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorConvert.java new file mode 100644 index 00000000000..6acaabd599a --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorConvert.java @@ -0,0 +1,65 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util.iterator; + +import java.util.function.Function; + +import org.apache.jena.atlas.io.IndentedWriter; +import org.apache.jena.atlas.lib.Lib; +import org.apache.jena.sparql.serializer.SerializationContext; + +/** Iterator over another QueryIterator, applying a converter function + * to each object that is returned by .next() */ +public class AbortableIteratorConvert extends AbortableIterator1 { + private Function converter ; + + public AbortableIteratorConvert(AbortableIterator iter, Function c) + { + super(iter); + this.converter = c ; + } + + @Override + public boolean hasNextBinding() + { + return getInput().hasNext() ; + } + + @Override + public O moveToNextBinding() + { + return converter.apply(getInput().next()) ; + } + + @Override + public void output(IndentedWriter out, SerializationContext sCxt) { + out.println(Lib.className(this)) ; + } + + @Override + protected void requestSubCancel() { + } + + @Override + protected void closeSubIterator() { + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorOverIterator.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorOverIterator.java new file mode 100644 index 00000000000..7a78478975a --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorOverIterator.java @@ -0,0 +1,70 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util.iterator; + +import java.util.Iterator; + +import org.apache.jena.atlas.io.IndentedWriter; +import org.apache.jena.atlas.lib.Lib; +import org.apache.jena.sparql.serializer.SerializationContext; + +public class AbortableIteratorOverIterator> + extends AbortableIteratorBase +{ + protected I iterator; + + public AbortableIteratorOverIterator(I iterator) { + super(); + this.iterator = iterator; + } + + I delegate() { + return iterator; + } + + @Override + public void output(IndentedWriter out, SerializationContext sCxt) { + out.println(Lib.className(this) + "/" + Lib.className(iterator)); +// out.incIndent(); +// iterator.output(out, sCxt); +// out.decIndent(); + // out.println(Utils.className(this)+"/"+Utils.className(iterator)) ; + } + + @Override + protected boolean hasNextBinding() { + return iterator.hasNext(); + } + + @Override + protected T moveToNextBinding() { + return iterator.next(); + } + + @Override + protected void closeIterator() { + } + + @Override + protected void requestCancel() { + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorOverQueryIterator.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorOverQueryIterator.java new file mode 100644 index 00000000000..fdfb49bbe57 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorOverQueryIterator.java @@ -0,0 +1,81 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util.iterator; + +import org.apache.jena.atlas.io.IndentedWriter; +import org.apache.jena.atlas.lib.Lib; +import org.apache.jena.sparql.engine.QueryIterator; +import org.apache.jena.sparql.engine.binding.Binding; +import org.apache.jena.sparql.serializer.SerializationContext; + +public class AbortableIteratorOverQueryIterator + extends AbortableIteratorBase +{ + protected QueryIterator iterator; + + public AbortableIteratorOverQueryIterator(QueryIterator qIter) { + iterator = qIter; + } + + QueryIterator delegate() { + return iterator; + } + + @Override + protected boolean hasNextBinding() { + return iterator.hasNext(); + } + + @Override + protected Binding moveToNextBinding() { + return iterator.nextBinding(); + } + + @Override + protected void closeIterator() { + if ( iterator != null ) { + iterator.close(); + iterator = null; + } + } + + @Override + protected void requestCancel() { + if ( iterator != null ) { + iterator.cancel(); + } + } + + @Override + public void output(IndentedWriter out) { + iterator.output(out); + } + + @Override + public void output(IndentedWriter out, SerializationContext sCxt) { + out.println(Lib.className(this) + "/" + Lib.className(iterator)); + out.incIndent(); + iterator.output(out, sCxt); + out.decIndent(); + // out.println(Utils.className(this)+"/"+Utils.className(iterator)) ; + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorPeek.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorPeek.java new file mode 100644 index 00000000000..4ddb058ef28 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorPeek.java @@ -0,0 +1,80 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util.iterator; + +import org.apache.jena.sparql.ARQInternalErrorException; + +public class AbortableIteratorPeek + extends AbortableIterator1 +{ + private T binding = null ; + private boolean closed = false ; + + public AbortableIteratorPeek(AbortableIterator iterator) { + super(iterator); + } + + /** Returns the next binding without moving on. Returns "null" for no such element. */ + public T peek() + { + if ( closed ) return null ; + if ( ! hasNextBinding() ) + return null ; + return binding ; + } + + @Override + protected boolean hasNextBinding() + { + if ( binding != null ) + return true ; + if ( ! getInput().hasNext() ) + return false ; + binding = getInput().nextBinding() ; + return true ; + } + + @Override + protected T moveToNextBinding() + { + if ( ! hasNextBinding() ) + throw new ARQInternalErrorException("No next binding") ; + T b = binding ; + binding = null ; + return b ; + } + + @Override + protected void closeSubIterator() { + this.closed = true; + } + + @Override + protected void requestSubCancel() { + } + +// @Override +// public void output(IndentedWriter out, SerializationContext sCxt) { +// // TODO Auto-generated method stub +// +// } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorWrapper.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorWrapper.java new file mode 100644 index 00000000000..903879da280 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIteratorWrapper.java @@ -0,0 +1,80 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util.iterator; + +import org.apache.jena.atlas.io.IndentedWriter; +import org.apache.jena.atlas.lib.Lib; +import org.apache.jena.sparql.serializer.SerializationContext; + +public class AbortableIteratorWrapper + extends AbortableIteratorBase +{ + private AbortableIterator delegate; + + public AbortableIteratorWrapper(AbortableIterator delegate) { + this.delegate = delegate; + } + + protected AbortableIterator getDelegate() { + return delegate; + } + + @Override + protected boolean hasNextBinding() { + return getDelegate().hasNext(); + } + + @Override + protected T moveToNextBinding() { + return getDelegate().nextBinding(); + } + + @Override + protected void closeIterator() { + AbortableIterator d = getDelegate(); + if (d != null) { + d.close(); + } + } + + @Override + protected void requestCancel() { + AbortableIterator d = getDelegate(); + if (d != null) { + d.cancel(); + } + } + + @Override + public void output(IndentedWriter out) { + getDelegate().output(out); + } + + @Override + public void output(IndentedWriter out, SerializationContext sCxt) { + out.println(Lib.className(this) + "/" + Lib.className(getDelegate())); + out.incIndent(); + getDelegate().output(out, sCxt); + out.decIndent(); + // out.println(Utils.className(this)+"/"+Utils.className(iterator)) ; + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIterators.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIterators.java new file mode 100644 index 00000000000..5bdd4c2dd86 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbortableIterators.java @@ -0,0 +1,78 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util.iterator; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Objects; + +import org.apache.jena.atlas.lib.Closeable; +import org.apache.jena.sparql.engine.QueryIterator; +import org.apache.jena.sparql.engine.binding.Binding; + +public class AbortableIterators { + public static AbortableIterator empty() { + return wrap(Collections.emptyIterator()); + } + + /** Bridge between {@code QueryIterator} and {@code AbortableIterator}. */ + public static AbortableIterator adapt(QueryIterator qIter) { + return new AbortableIteratorOverQueryIterator(qIter); + } + + public static AbortableIterator wrap(Iterator it) { + return it instanceof AbortableIterator ait + ? ait + : new AbortableIteratorOverIterator<>(it); + } + + public static QueryIterator asQueryIterator(AbortableIterator it) { + return it instanceof AbortableIteratorOverQueryIterator x + ? x.delegate() + : new QueryIteratorOverAbortableIterator(it); + } + + public static AbortableIterator concat(AbortableIterator a, AbortableIterator b) { + AbortableIteratorConcat result = new AbortableIteratorConcat<>(); + result.add(a); + result.add(b); + return result; + } + + /** Wrap an {@link AbortableIterator} with an additional close action. */ + public static AbortableIterator onClose(AbortableIterator qIter, Closeable action) { + Objects.requireNonNull(qIter); + AbortableIterator result = action == null + ? qIter + : new AbortableIteratorWrapper<>(qIter) { + @Override + protected void closeIterator() { + try { + action.close(); + } finally { + super.closeIterator(); + } + } + }; + return result; + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbstractAbortableIterator.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbstractAbortableIterator.java new file mode 100644 index 00000000000..8816b6c2c38 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/AbstractAbortableIterator.java @@ -0,0 +1,105 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util.iterator; + +import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.jena.atlas.lib.Lib; + +public abstract class AbstractAbortableIterator + extends AbortableIteratorBase +{ + private boolean slotIsSet = false; + private boolean hasMore = true; + private T slot = null; + + public AbstractAbortableIterator() { + super(); + } + + public AbstractAbortableIterator(AtomicBoolean cancelSignal) { + super(cancelSignal); + } + + @Override + protected final boolean hasNextBinding() { + if ( slotIsSet ) + return true; + + if (!hasMore) { + return false; + } + // boolean r = hasMore(); + // if ( !r ) { + // close(); + // return false; + // } + + slot = moveToNext(); + // if ( slot == null ) { + if (!hasMore) { + close(); + return false; + } + + slotIsSet = true; + return true; + } + + protected final T endOfData() { + hasMore = false; + return null; + } + + @Override + public final T moveToNextBinding() { + if ( !hasNext() ) + throw new NoSuchElementException(Lib.className(this)); + + T obj = slot; + slot = null; + slotIsSet = false; + return obj; + } + + @Override + protected final void closeIterator() { + // Called by QueryIterBase.close() + slotIsSet = false; + slot = null; + + closeIteratorActual(); + } + + protected abstract void closeIteratorActual(); + +// @Override +// protected void requestCancel() { +// } + + /** + * Method that must return the next non-null element. + * A return value of null indicates that the iterator's end has been reached. + */ + protected abstract T moveToNext(); +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/QueryIterOverAbortableIterator.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/QueryIterOverAbortableIterator.java new file mode 100644 index 00000000000..24fcf529877 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/QueryIterOverAbortableIterator.java @@ -0,0 +1,58 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util.iterator; + +import org.apache.jena.sparql.engine.ExecutionContext; +import org.apache.jena.sparql.engine.binding.Binding; +import org.apache.jena.sparql.engine.iterator.QueryIter; + +/** QueryIter-based wrapper which tracks open iterators in the execution context. */ +public class QueryIterOverAbortableIterator + extends QueryIter +{ + private AbortableIterator delegate; + + public QueryIterOverAbortableIterator(ExecutionContext execCxt, AbortableIterator delegate) { + super(execCxt); + this.delegate = delegate; + } + + @Override + protected boolean hasNextBinding() { + return delegate.hasNext(); + } + + @Override + protected Binding moveToNextBinding() { + return delegate.next(); + } + + @Override + protected void closeIterator() { + delegate.close(); + } + + @Override + protected void requestCancel() { + delegate.cancel(); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/QueryIteratorMaterialize.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/QueryIteratorMaterialize.java new file mode 100644 index 00000000000..41a0795fc29 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/QueryIteratorMaterialize.java @@ -0,0 +1,106 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util.iterator; + +import org.apache.jena.atlas.data.BagFactory; +import org.apache.jena.atlas.data.DataBag; +import org.apache.jena.atlas.data.ThresholdPolicy; +import org.apache.jena.atlas.data.ThresholdPolicyFactory; +import org.apache.jena.sparql.engine.ExecutionContext; +import org.apache.jena.sparql.engine.QueryIterator; +import org.apache.jena.sparql.engine.binding.Binding; +import org.apache.jena.sparql.engine.iterator.QueryIterPlainWrapper; +import org.apache.jena.sparql.engine.iterator.QueryIteratorWrapper; +import org.apache.jena.sparql.system.SerializationFactoryFinder; +import org.apache.jena.sparql.util.Context; + +/** + * A QueryIterator that upon access of the first item consumes the underlying + * iterator into a list. Supports abort during materialization. + */ +public class QueryIteratorMaterialize extends QueryIteratorWrapper { + protected QueryIterator outputIt = null; + protected ExecutionContext execCxt; + + /** If the threshold policy is not set then it will be lazily initialized from the execCxt */ + protected ThresholdPolicy thresholdPolicy; + + public QueryIteratorMaterialize(QueryIterator qIter, ExecutionContext execCxt) { + this(qIter, execCxt, null); + } + + /** Ctor with a fixed threshold policy. */ + public QueryIteratorMaterialize(QueryIterator qIter, ExecutionContext execCxt, ThresholdPolicy thresholdPolicy) { + super(qIter); + this.execCxt = execCxt; + this.thresholdPolicy = thresholdPolicy; + } + + /** + * Get the threshold policy. + * May return null if it was not initialized yet. + * Call {@link #hasNext()} to force initialization. + */ + public ThresholdPolicy getThresholdPolicy() { + return thresholdPolicy; + } + + @Override + protected boolean hasNextBinding() { + collect(); + return outputIt.hasNext(); + } + + @Override + protected Binding moveToNextBinding() { + collect(); + Binding b = outputIt.next(); + return b; + } + + protected void collect() { + if (outputIt == null) { + Context cxt = execCxt.getContext(); + if (thresholdPolicy == null) { + thresholdPolicy = ThresholdPolicyFactory.policyFromContext(cxt); + } + DataBag db = BagFactory.newDefaultBag(thresholdPolicy, SerializationFactoryFinder.bindingSerializationFactory()); + try { + db.addAll(iterator); + } finally { + iterator.close(); + } + outputIt = QueryIterPlainWrapper.create(db.iterator(), execCxt); + } + } + + @Override + protected void closeIterator() { + // If the output iterator is set, then the input iterator has been consumed and closed. + if (outputIt != null) { + outputIt.close(); + } else { + // Output iterator was not created -> close the input iterator. + super.closeIterator(); + } + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/QueryIteratorOverAbortableIterator.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/QueryIteratorOverAbortableIterator.java new file mode 100644 index 00000000000..eb2f9677ba4 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/impl/util/iterator/QueryIteratorOverAbortableIterator.java @@ -0,0 +1,67 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.apache.jena.sparql.service.enhancer.impl.util.iterator; + +import org.apache.jena.atlas.io.IndentedWriter; +import org.apache.jena.sparql.engine.binding.Binding; +import org.apache.jena.sparql.engine.iterator.QueryIteratorBase; +import org.apache.jena.sparql.serializer.SerializationContext; + +public class QueryIteratorOverAbortableIterator + extends QueryIteratorBase +{ + private AbortableIterator delegate; + + public QueryIteratorOverAbortableIterator(AbortableIterator delegate) { + super(); + this.delegate = delegate; + } + + protected AbortableIterator delegate() { + return delegate; + } + + @Override + public void output(IndentedWriter out, SerializationContext sCxt) { + delegate().output(out, sCxt); + } + + @Override + protected boolean hasNextBinding() { + return delegate().hasNext(); + } + + @Override + protected Binding moveToNextBinding() { + return delegate().next(); + } + + @Override + protected void closeIterator() { + delegate().close(); + } + + @Override + protected void requestCancel() { + delegate().cancel(); + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/init/ServiceEnhancerConstants.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/init/ServiceEnhancerConstants.java index 0612684a665..368d42336c8 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/init/ServiceEnhancerConstants.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/init/ServiceEnhancerConstants.java @@ -31,12 +31,18 @@ public class ServiceEnhancerConstants { /** An IRI constant for referencing the active dataset within a SERVICE clause */ public static final Node SELF = NodeFactory.createURI("urn:x-arq:self"); + /** An IRI constant for referencing the active dataset within a SERVICE clause */ + // FIXME SERVICE is handled by the the bulk chain of the service executor which receives an iterator of the input bindings. + // In constrast SERVICE is handled by the single chain which is fed each binding individually. + // Self bulk can thus perform more powerful bulk requests. + public static final Node SELF_BULK = NodeFactory.createURI("urn:x-arq:self+bulk"); + /** Namespace for context symbols. Same as the assembler vocabulary. */ public static final String NS = ServiceEnhancerVocab.NS; public static String getURI() { return NS; } - /** Maximum number of bindings to group into a single bulk request; restricts serviceBulkRequestItemCount */ + /** Maximum number of bindings to group into a single bulk request; upper limit for serviceBulkRequestBindingCount */ public static final Symbol serviceBulkMaxBindingCount = SystemARQ.allocSymbol(NS, "serviceBulkMaxBindingCount") ; /** Maximum number of out-of-band bindings that can be skipped over when forming an individual bulk request */ @@ -45,9 +51,30 @@ public class ServiceEnhancerConstants { /** Number of bindings to group into a single bulk request */ public static final Symbol serviceBulkBindingCount = SystemARQ.allocSymbol(NS, "serviceBulkBindingCount") ; + /** Default number of slots when no explicit number is given. + * Subject to capping by {@link #serviceConcurrentMaxSlotCount}. */ + public static final Symbol serviceConcurrentDftSlotCount = SystemARQ.allocSymbol(NS, "serviceConcurrentDftSlotCount") ; + + public static final Symbol serviceConcurrentMaxSlotCount = SystemARQ.allocSymbol(NS, "serviceConcurrentMaxSlotCount") ; + + /** Default number of slots when no explicit number is given. + * Subject to capping by {@link #serviceConcurrentDftReadaheadCount}. */ + public static final Symbol serviceConcurrentDftReadaheadCount = SystemARQ.allocSymbol(NS, "serviceConcurrentDftReadaheadCount") ; + + public static final Symbol serviceConcurrentMaxReadaheadCount = SystemARQ.allocSymbol(NS, "serviceConcurrentMaxReadaheadCount") ; + /** Symbol for the cache of services' result sets */ public static final Symbol serviceCache = SystemARQ.allocSymbol(NS, "serviceCache") ; + /** Factory for on-demand initialization of a serviceCache instance */ + // public static final Symbol serviceCacheFactory = SystemARQ.allocSymbol(NS, "serviceCacheFactory") ; + + // The following serviceCache* context symbols can be used to configure on-demand serviceCache instance creation. + + public static final Symbol serviceCacheMaxEntryCount = SystemARQ.allocSymbol(NS, "serviceCacheMaxEntryCount") ; + public static final Symbol serviceCachePageSize = SystemARQ.allocSymbol(NS, "serviceCachePageSize") ; + public static final Symbol serviceCacheMaxPageCount = SystemARQ.allocSymbol(NS, "serviceCacheMaxPageCount") ; + /** Symbol for the cache of services' result set sizes */ public static final Symbol serviceResultSizeCache = SystemARQ.allocSymbol(NS, "serviceResultSizeCache") ; diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/init/ServiceEnhancerInit.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/init/ServiceEnhancerInit.java index bb9ccaa5288..e82458378be 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/init/ServiceEnhancerInit.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/init/ServiceEnhancerInit.java @@ -21,8 +21,15 @@ package org.apache.jena.sparql.service.enhancer.init; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; import java.util.Map; +import java.util.Map.Entry; +import java.util.NavigableSet; import java.util.Set; +import java.util.stream.Collectors; import org.apache.jena.assembler.Assembler; import org.apache.jena.assembler.assemblers.AssemblerGroup; @@ -52,33 +59,47 @@ import org.apache.jena.sparql.engine.QueryIterator; import org.apache.jena.sparql.engine.Rename; import org.apache.jena.sparql.engine.binding.Binding; +import org.apache.jena.sparql.engine.binding.BindingBuilder; import org.apache.jena.sparql.engine.binding.BindingFactory; -import org.apache.jena.sparql.engine.iterator.QueryIter; import org.apache.jena.sparql.engine.iterator.QueryIterCommonParent; +import org.apache.jena.sparql.engine.iterator.QueryIterProject; import org.apache.jena.sparql.engine.iterator.QueryIteratorMapped; +import org.apache.jena.sparql.engine.iterator.QueryIteratorWrapper; import org.apache.jena.sparql.engine.main.QC; import org.apache.jena.sparql.function.FunctionRegistry; +import org.apache.jena.sparql.graph.NodeTransform; +import org.apache.jena.sparql.graph.NodeTransformLib; import org.apache.jena.sparql.pfunction.PropertyFunctionRegistry; import org.apache.jena.sparql.service.ServiceExecutorRegistry; +import org.apache.jena.sparql.service.bulk.ChainingServiceExecutorBulk; import org.apache.jena.sparql.service.enhancer.algebra.TransformSE_EffectiveOptions; import org.apache.jena.sparql.service.enhancer.algebra.TransformSE_JoinStrategy; import org.apache.jena.sparql.service.enhancer.assembler.DatasetAssemblerServiceEnhancer; import org.apache.jena.sparql.service.enhancer.assembler.ServiceEnhancerVocab; import org.apache.jena.sparql.service.enhancer.function.cacheRm; +import org.apache.jena.sparql.service.enhancer.impl.ChainingServiceExecutorBulkConcurrent; import org.apache.jena.sparql.service.enhancer.impl.ChainingServiceExecutorBulkServiceEnhancer; import org.apache.jena.sparql.service.enhancer.impl.ServiceOpts; +import org.apache.jena.sparql.service.enhancer.impl.ServiceOptsSE; import org.apache.jena.sparql.service.enhancer.impl.ServiceResponseCache; import org.apache.jena.sparql.service.enhancer.impl.ServiceResultSizeCache; import org.apache.jena.sparql.service.enhancer.impl.util.DynamicDatasetUtils; +import org.apache.jena.sparql.service.enhancer.impl.util.Lazy; import org.apache.jena.sparql.service.enhancer.impl.util.VarScopeUtils; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.QueryIteratorMaterialize; import org.apache.jena.sparql.service.enhancer.pfunction.cacheLs; import org.apache.jena.sparql.service.single.ChainingServiceExecutor; import org.apache.jena.sparql.util.Context; +import org.apache.jena.sparql.util.MappingRegistry; import org.apache.jena.sys.JenaSubsystemLifecycle; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ServiceEnhancerInit implements JenaSubsystemLifecycle { + private static final Logger logger = LoggerFactory.getLogger(ServiceEnhancerInit.class); + @Override public void start() { init(); @@ -89,16 +110,65 @@ public void stop() { // Nothing to do } + public static void initMappingRegistry() { + // Register the "se" prefix for use with cli options, such as ./arq --set 'se:serviceCacheMaxEntryCount=1000' + MappingRegistry.addPrefixMapping("se", ServiceEnhancerVocab.getURI()); + } + + /** + * Initialize the SERVICE <collect:> { }. + * This collects all bindings of the graph pattern into a list and serves them + * from the list. + */ + public static void initFeatureCollect() { + String collectOptName = "collect"; + ServiceExecutorRegistry.get().addBulkLink((opService, input, execCxt, chain) -> { + QueryIterator r; + ServiceOpts opts = ServiceOpts.getEffectiveService(opService, ServiceEnhancerConstants.SELF.getURI(), key -> key.equals("collect")); + if (opts.containsKey(collectOptName)) { + opts.removeKey(collectOptName); + OpService newOp = opts.toService(); + QueryIterator tmp = chain.createExecution(newOp, input, execCxt); + r = new QueryIteratorMaterialize(tmp, execCxt); + } else { + r = chain.createExecution(opService, input, execCxt); + } + return r; + }); + } + + public static void initFeatureConcurrent() { + ServiceExecutorRegistry.get().addBulkLink(new ChainingServiceExecutorBulkConcurrent("concurrent")); + } + + public static void init() { - ServiceResponseCache cache = new ServiceResponseCache(); - ARQ.getContext().put(ServiceEnhancerConstants.serviceCache, cache); + initMappingRegistry(); + + Context cxt = ARQ.getContext(); + + initFeatureConcurrent(); + initFeatureCollect(); + + // Creation of the cache is deferred until first use. + // This allows for the cache creation to read settings from the context. + Lazy cache = Lazy.of(() -> { + ServiceResponseCache.SimpleConfig conf = ServiceResponseCache.buildConfig(cxt); + ServiceResponseCache r = new ServiceResponseCache(conf); + if (logger.isInfoEnabled()) { + logger.info("Initialized Service Enhancer Cache with config {}", conf); + } + return r; + }); + cxt.put(ServiceEnhancerConstants.serviceCache, cache); ServiceResultSizeCache resultSizeCache = new ServiceResultSizeCache(); - ServiceResultSizeCache.set(ARQ.getContext(), resultSizeCache); + ServiceResultSizeCache.set(cxt, resultSizeCache); ServiceExecutorRegistry.get().addBulkLink(new ChainingServiceExecutorBulkServiceEnhancer()); // Register SELF extension + registerServiceExecutorBulkSelf(ServiceExecutorRegistry.get()); registerServiceExecutorSelf(ServiceExecutorRegistry.get()); registerWith(Assembler.general()); @@ -118,17 +188,160 @@ public static void registerPFunctions(PropertyFunctionRegistry reg) { reg.put(cacheLs.DEFAULT_IRI, cacheLs.class); } + public static void registerServiceExecutorBulkSelf(ServiceExecutorRegistry registry) { + ChainingServiceExecutorBulk selfExec = (opExec, rawInput, execCxt, chain) -> { + QueryIterator r; + ServiceOpts so = ServiceOptsSE.getEffectiveService(opExec); + OpService target = so.getTargetService(); + + // Remove path variables from the input binding + QueryIterator input = new QueryIteratorWrapper(rawInput) { + @Override + public Binding moveToNextBinding() { + Binding rawBinding = super.moveToNextBinding(); + Iterator it = rawBinding.vars(); + boolean hasPathVar = false; + while (it.hasNext()) { + Var v = it.next(); + if (v.getName().startsWith(ARQConstants.allocPathVariables)) { + hasPathVar = true; + break; + } + } + + Binding rr; + if (hasPathVar) { + BindingBuilder bb = BindingFactory.builder(); + rawBinding.forEach((v, n) -> { + if (!v.getName().startsWith(ARQConstants.allocPathVariables)) { + bb.add(v, n); + } + }); + rr = bb.build(); + } else { + rr = rawBinding; + } + return rr; + } + }; + + // Issue: Because of the service clause, the optimizer has not yet been run. + // - in order to have property functions recognized properly. + // - However: We must not touch scoping or we will break a prior SERVICE . + // - Also: TransformPathFlatten may introduce new variables that clash resulting in incompatible bindings. + // Current workaround: remove those variables from the input binding. + + if (ServiceEnhancerConstants.SELF_BULK.equals(target.getService())) { + String optimizerMode = so.getFirstValue(ServiceOptsSE.SO_OPTIMIZE, "on", "on"); + Op op = opExec.getSubOp(); + Op tmpOp = op; + Op finalOp = tmpOp; + + boolean useQc = true; + if (useQc) { + + // Run the optimizer unless disabled + + if (!"off".equals(optimizerMode)) { + // if (false) { + + // Here we try to protect loop variables from getting renamed + // by the optimizer. + // This code first saves the scope levels of variables, + // then runs the optimizer, and then restores the scope levels again. + Collection opVars = OpVars.mentionedVars(op); + Map> opVarLevels = VarScopeUtils.getScopeLevels(opVars); + + Map opVarMinLevels = opVarLevels.entrySet().stream() + .collect(Collectors.toMap(Entry::getKey, e -> e.getValue().first())); + + // Variables that only appear on the same level may be loop vars + Set candidateLoopVars = opVarLevels.entrySet().stream() + .filter(e -> e.getValue().size() == 1) + .map(Entry::getKey) + .collect(Collectors.toSet()); + + // Run the optimizer. + // TransformPathFlatten may introduce variables that did not exist before. + Context cxt = execCxt.getContext(); + RewriteFactory rf = decideOptimizer(cxt); + Rewrite rw = rf.create(cxt); + tmpOp = rw.rewrite(op); + + Collection newOpVars = OpVars.mentionedVars(tmpOp); + Map newOpVarMinLevels = VarScopeUtils.getMinimumScopeLevels(newOpVars); + + NodeTransform nf = node -> { + Node rNode; + if (Var.isVar(node)) { + Var v = Var.alloc(node); + String vn = v.getName(); + String plainName = VarScopeUtils.getPlainName(vn); + Integer oldMinLevel = opVarMinLevels.get(plainName); + + if (oldMinLevel != null) { + if (candidateLoopVars.contains(plainName)) { + // If the variable appeared only at exactly 1 level + // then restore that level. + rNode = VarScopeUtils.allocScoped(plainName, oldMinLevel); + } else { + // Variable appeared at different scopes - subtract the delta. + Integer newMinLevel = newOpVarMinLevels.get(plainName); + if (newMinLevel != null) { + int newLevel = VarScopeUtils.getScopeLevel(vn); + int delta = newMinLevel - oldMinLevel; + int finalLevel = newLevel - delta; + rNode = VarScopeUtils.allocScoped(plainName, finalLevel); + } else { + // Variable name disappeared after rewrite - should not happen. + // But retain node as is. + rNode = node; + } + } + } else { + rNode = node; + } + } else { + rNode = node; + } + return rNode; + }; + + finalOp = NodeTransformLib.transform(nf, tmpOp); + } + // Using QC with e.g. TDB2 breaks unionDefaultGraph mode. + // Issue seems to be mitigated going through QueryEngineRegistry. + + // The issue now is that PathCompile may introduce clashing variables. + // We could apply path transformation now and rename those vars. + r = QC.execute(finalOp, input, execCxt); + } else { + // A context copy is needed in order to isolate changes from further executions; + // without a copy query engines may e.g. overwrite the context value for the NOW() function. + // Context cxtCopy = execCxt.getContext().copy(); + // r = execute(op, dataset, input, cxtCopy); + throw new RuntimeException("Cannot go through query engine factory for bulk requests."); + } + } else { + r = chain.createExecution(opExec, input, execCxt); + } + return r; + }; + // registry.addBulkLink(selfExec); + registry.getBulkChain().add(selfExec); + } + public static void registerServiceExecutorSelf(ServiceExecutorRegistry registry) { ChainingServiceExecutor selfExec = (opExec, opOrig, binding, execCxt, chain) -> { QueryIterator r; - ServiceOpts so = ServiceOpts.getEffectiveService(opExec); + ServiceOpts so = ServiceOptsSE.getEffectiveService(opExec); OpService target = so.getTargetService(); DatasetGraph dataset = execCxt.getDataset(); // It seems that we always need to run the optimizer here // in order to have property functions recognized properly if (ServiceEnhancerConstants.SELF.equals(target.getService())) { - String optimizerMode = so.getFirstValue(ServiceOpts.SO_OPTIMIZE, "on", "on"); + String optimizerMode = so.getFirstValue(ServiceOptsSE.SO_OPTIMIZE, "on", "on"); Op op = opExec.getSubOp(); boolean useQc = false; @@ -147,7 +360,7 @@ public static void registerServiceExecutorSelf(ServiceExecutorRegistry registry) // A context copy is needed in order to isolate changes from further executions; // without a copy query engines may e.g. overwrite the context value for the NOW() function. Context cxtCopy = execCxt.getContext().copy(); - r = execute(op, dataset, binding, cxtCopy); + r = execute(op, dataset, binding, cxtCopy, execCxt); } } else { r = chain.createExecution(opExec, opOrig, binding, execCxt); @@ -157,11 +370,19 @@ public static void registerServiceExecutorSelf(ServiceExecutorRegistry registry) registry.addSingleLink(selfExec); } - /** Special processing that unwraps dynamic datasets */ - private static QueryIterator execute(Op op, DatasetGraph dataset, Binding binding, Context cxt) { + /** + * Special processing that unwraps dynamic datasets. + * Returns a QueryIterCommonParent with the given input binding. If the tracker is non-null then the returned iterator will be tracked in it. + * + * Note, that the execCxt of the sub-execution must generally remain isolated from the execCxt of the parent-execution (the tracker). + * For example, sub-executions will create their own QueryIteratorCheck which upon close will check for any non-closed iterators tracked in their + * execCxt. This means, that if QueryIteratorCheck is not a top-level QueryIter but somehow nested, it will complain if there exists an ancestor + * iterator that has not yet been closed. Non-closed ancestors can happen as a result of concurrent cancel: A child iterator may close itself in + * reaction to the cancel signal, whereas its parent iterator may not have it seen yet. + */ + private static QueryIterator execute(Op op, DatasetGraph dataset, Binding binding, Context cxt, ExecutionContext tracker) { QueryIterator innerIter = null; QueryIterator outerIter = null; - ExecutionContext execCxt = null; DynamicDatasetGraph ddg = DynamicDatasetUtils.asUnwrappableDynamicDatasetOrNull(dataset); if (ddg != null) { @@ -169,7 +390,8 @@ private static QueryIterator execute(Op op, DatasetGraph dataset, Binding bindin // Set up the map that allows for mapping the query's result set variables's // to the appropriately scoped ones Set visibleVars = OpVars.visibleVars(op); - Map normedToScoped = VarScopeUtils.normalizeVarScopes(visibleVars).inverse(); + Set visiblePlainNames = VarScopeUtils.getPlainNames(visibleVars); + Map normedToScoped = VarScopeUtils.normalizeVarScopes(visibleVars, visiblePlainNames).inverse(); Op opRestored = Rename.reverseVarRename(op, true); Query baseQuery = OpAsQuery.asQuery(opRestored); @@ -189,17 +411,25 @@ private static QueryIterator execute(Op op, DatasetGraph dataset, Binding bindin if (innerIter == null) { QueryEngineFactory qef = QueryEngineRegistry.findFactory(op, dataset, cxt); Plan plan = qef.create(op, dataset, BindingFactory.empty(), cxt); + + // Note: The default plan implementation returns an iterator + // that closes the plan when closing the iterator. innerIter = plan.iterator(); + + // If the op contains an OpPath then variables such as ??P0 + // may get introduced and projected - which we must filter out. + List visibleVars = new ArrayList<>(OpVars.visibleVars(op)); + innerIter = QueryIterProject.create(innerIter, visibleVars, tracker); + outerIter = innerIter; } - execCxt = innerIter instanceof QueryIter ? ((QueryIter)innerIter).getExecContext() : null; - QueryIterator result = new QueryIterCommonParent(outerIter, binding, execCxt); + // execCxt = innerIter instanceof QueryIter ? ((QueryIter)innerIter).getExecContext() : null; + QueryIterator result = new QueryIterCommonParent(outerIter, binding, tracker); return result; } - static void registerWith(AssemblerGroup g) - { + static void registerWith(AssemblerGroup g) { AssemblerUtils.register(g, ServiceEnhancerVocab.DatasetServiceEnhancer, new DatasetAssemblerServiceEnhancer(), DatasetAssembler.getGeneralType()); // Note: We can't install the plugin on graphs because they don't have a context @@ -272,16 +502,19 @@ public static Node resolveServiceNode(Node node, ExecutionContext execCxt) { return result; } + /** + * Return the value for {@link ServiceEnhancerConstants#datasetId} in the context. + * If it is null then instead return an IRI that includes the involved dataset's + * system identity hash code. + */ public static Node resolveSelfId(ExecutionContext execCxt) { Context context = execCxt.getContext(); - Node id = context.get(ServiceEnhancerConstants.datasetId); if (id == null) { DatasetGraph dg = execCxt.getDataset(); int hashCode = System.identityHashCode(dg); id = NodeFactory.createLiteralString(ServiceEnhancerConstants.SELF.getURI() + "@dataset" + hashCode); } - return id; } } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/pfunction/cacheLs.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/pfunction/cacheLs.java index 670f077bb74..a687381cd05 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/pfunction/cacheLs.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/pfunction/cacheLs.java @@ -30,12 +30,9 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.concurrent.locks.Lock; import java.util.function.Supplier; import java.util.stream.Stream; -import com.google.common.collect.Range; -import com.google.common.collect.Sets; import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; import org.apache.jena.query.Query; @@ -55,15 +52,18 @@ import org.apache.jena.sparql.pfunction.PropertyFunctionEval; import org.apache.jena.sparql.service.enhancer.assembler.ServiceEnhancerVocab; import org.apache.jena.sparql.service.enhancer.claimingcache.RefFuture; +import org.apache.jena.sparql.service.enhancer.concurrent.AutoLock; import org.apache.jena.sparql.service.enhancer.impl.ServiceCacheKey; import org.apache.jena.sparql.service.enhancer.impl.ServiceCacheValue; import org.apache.jena.sparql.service.enhancer.impl.ServiceResponseCache; import org.apache.jena.sparql.service.enhancer.impl.util.PropFuncArgUtils; -import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerConstants; import org.apache.jena.sparql.service.enhancer.slice.api.Slice; import org.apache.jena.sparql.util.Context; import org.apache.jena.sparql.util.NodeFactoryExtra; +import com.google.common.collect.Range; +import com.google.common.collect.Sets; + /** * A property function for listing the cache's content. @@ -106,7 +106,7 @@ public QueryIterator execEvaluated(Binding inputBinding, PropFuncArg subject, No ExecutionContext execCxt) { Context context = execCxt.getContext(); - ServiceResponseCache cache = context.get(ServiceEnhancerConstants.serviceCache); + ServiceResponseCache cache = ServiceResponseCache.get(context); Node s = subject.getArg(); Var sv = s instanceof Var ? (Var)s : null; @@ -163,12 +163,8 @@ public QueryIterator execEvaluated(Binding inputBinding, PropFuncArg subject, No if (refFuture != null) { ServiceCacheValue entry = refFuture.await(); Slice slice = entry.getSlice(); - Lock lock = slice.getReadWriteLock().readLock(); - lock.lock(); - try { + try (AutoLock lock = AutoLock.lock(slice.getReadWriteLock().readLock())) { ranges = new ArrayList<>(entry.getSlice().getLoadedRanges().asRanges()); - } finally { - lock.unlock(); } if (ranges.isEmpty()) { diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/ArrayOps.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/ArrayOps.java index 5dc73bed2bb..361ea822ad8 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/ArrayOps.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/ArrayOps.java @@ -39,6 +39,9 @@ public interface ArrayOps { void fill(A array, int offset, int length, Object value); void copy(A src, int srcPos, A dest, int destPos, int length); + + String toString(A array); + Object getDefaultValue(); @SuppressWarnings("unchecked") @@ -66,6 +69,11 @@ default void lengthRaw(Object array) { length((A)array); } + @SuppressWarnings("unchecked") + default String toStringRaw(Object array) { + return toString((A)array); + } + // TODO Cache with a ClassInstanceMap? @SuppressWarnings("unchecked") public static ArrayOpsObject createFor(Class componentType) { diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/IteratorOverReadableChannel.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/IteratorOverReadableChannel.java index d96419a1a9e..65e06477309 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/IteratorOverReadableChannel.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/IteratorOverReadableChannel.java @@ -23,11 +23,11 @@ import java.io.IOException; +import org.apache.jena.atlas.iterator.IteratorCloseable; + import com.google.common.base.Preconditions; import com.google.common.collect.AbstractIterator; -import org.apache.jena.atlas.iterator.IteratorCloseable; - public class IteratorOverReadableChannel extends AbstractIterator implements IteratorCloseable diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/ReadableChannelOverSliceAccessor.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/ReadableChannelOverSliceAccessor.java index df666702b7f..7d59f08a13e 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/ReadableChannelOverSliceAccessor.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/ReadableChannelOverSliceAccessor.java @@ -48,6 +48,7 @@ public void closeActual() throws IOException { @Override public int read(A array, int position, int length) throws IOException { accessor.claimByOffsetRange(posInSlice, posInSlice + length); + // unsafeRead because the read was scheduled over cached data ranges that were protected from eviction. int result = accessor.unsafeRead(array, position, posInSlice, length); if (result > 0) { posInSlice += result; diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/Slice.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/Slice.java index 9bfbd23f272..c0df2c58aa9 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/Slice.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/Slice.java @@ -28,11 +28,13 @@ import java.util.function.Consumer; import java.util.function.Function; +import org.apache.jena.atlas.lib.Closeable; +import org.apache.jena.atlas.lib.Sync; +import org.apache.jena.sparql.service.enhancer.concurrent.AutoLock; + import com.google.common.collect.Range; import com.google.common.collect.RangeSet; -import org.apache.jena.atlas.lib.Sync; - /** * A concurrently accessible sequence of data of possibly unknown size. * @@ -52,7 +54,7 @@ public interface Slice * * This method should not be used directly but via {@link SliceAccessor#addEvictionGuard}. */ - Disposable addEvictionGuard(RangeSet range); + Closeable addEvictionGuard(RangeSet range); /** * Read the metadata and check whether the slice has a known size and @@ -95,17 +97,12 @@ default X computeFromMetaData(boolean isWrite, Function * * This method must be called after acquiring a read lock on the slice's metadata. * - * @param ranges The set of ranges which to protected from eviction + * @param ranges The set of ranges which to protect from eviction. */ void addEvictionGuard(RangeSet ranges); @@ -63,10 +63,9 @@ default void addEvictionGuard(Range range) { /** * Set or update the claimed range - this will immediately request references to any pages providing the data for that range. - * Pages outside of that range are considered as no longer needed pages will immediately be released. + * Pages outside of that range will be immediately be released. * * This method prepares the pages which can be subsequently locked. - * Calling this method while the page range is locked ({@link #lock()}) raises an {@link IllegalStateException}. * * @param startOffset * @param endOffset @@ -74,13 +73,11 @@ default void addEvictionGuard(Range range) { void claimByOffsetRange(long startOffset, long endOffset); /** - * Lock the range for writing - */ - void lock(); - - /** - * Put a sequence of items into the claimed range - * Attempts to put items outside of the claimed range raises an {@link IndexOutOfBoundsException} + * Put a sequence of data items into the claimed range. + * Any attempts to put items outside of the claimed range raises an {@link IndexOutOfBoundsException}. + * + * A write typically requires updating the slice metadata, so before calling this method, + * the slice itself should be write-locked using {@link #getSlice()} and {@link Slice#getReadWriteLock()}. * * The page range should be locked when calling this method. */ @@ -92,11 +89,6 @@ default void addEvictionGuard(Range range) { */ int unsafeRead(A tgt, int tgtOffset, long srcOffset, int length) throws IOException; - /** - * Unlock the range - */ - void unlock(); - /** * Releases all currently held pages. * Future requests via {@link #claimByOffsetRange(long, long)} are allowed. diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/SliceMetaDataBasic.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/SliceMetaDataBasic.java index 14f75d39aea..18e2dd489a5 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/SliceMetaDataBasic.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/api/SliceMetaDataBasic.java @@ -23,14 +23,19 @@ import java.util.List; +import org.apache.jena.sparql.service.enhancer.impl.util.RangeUtils; + import com.google.common.base.Preconditions; import com.google.common.collect.Range; import com.google.common.collect.RangeMap; import com.google.common.collect.RangeSet; import com.google.common.collect.TreeRangeSet; -import org.apache.jena.sparql.service.enhancer.impl.util.RangeUtils; - +/** + * Metadata about a sequence of data of possibly unknown size. + * Keeps track of which ranges of data have been loaded and the minimum/maximum known sizes. + * If the minimum and maximum size are equal then the size is considered known. + */ public interface SliceMetaDataBasic { RangeSet getLoadedRanges(); RangeMap> getFailedRanges(); @@ -63,6 +68,7 @@ default SliceMetaDataBasic updateMinimumKnownSize(long size) { return this; } + /** If minSize equals maxSize then its value is returned, -1 otherwise. */ default long getKnownSize() { long minSize = getMinimumKnownSize(); long maxSize = getMaximumKnownSize(); @@ -72,13 +78,12 @@ default long getKnownSize() { default SliceMetaDataBasic setKnownSize(long size) { Preconditions.checkArgument(size >= 0, "Negative known size"); - setMinimumKnownSize(size); setMaximumKnownSize(size); - return this; } + /** Get the ranges of missing data within the given range */ default RangeSet getGaps(Range requestRange) { long maxKnownSize = getMaximumKnownSize(); Range maxKnownRange = Range.closedOpen(0l, maxKnownSize); diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/ArrayOpsObject.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/ArrayOpsObject.java index e21d72886a1..867c9b6e806 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/ArrayOpsObject.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/ArrayOpsObject.java @@ -88,4 +88,9 @@ public void copy(Object[] src, int srcPos, Object[] dest, int destPos, int lengt public int length(Object[] array) { return array.length; } + + @Override + public String toString(T[] array) { + return Arrays.toString(array); + } } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/BufferOverArray.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/BufferOverArray.java index cb77ef1b2c6..e52078f53bd 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/BufferOverArray.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/BufferOverArray.java @@ -85,4 +85,9 @@ public void put(long offset, Object item) { public Object get(long index) { throw new UnsupportedOperationException(); } + + @Override + public String toString() { + return arrayOps.toString(array); + } } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/RangeBufferImpl.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/RangeBufferImpl.java index 71fb2c86c65..9b909b99882 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/RangeBufferImpl.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/RangeBufferImpl.java @@ -23,13 +23,12 @@ import java.io.IOException; -import com.google.common.collect.Range; -import com.google.common.collect.RangeSet; -import com.google.common.collect.TreeRangeSet; - import org.apache.jena.sparql.service.enhancer.impl.util.RangeUtils; import org.apache.jena.sparql.service.enhancer.slice.api.ArrayOps; +import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; +import com.google.common.collect.TreeRangeSet; public class RangeBufferImpl implements RangeBuffer @@ -118,14 +117,15 @@ public ArrayOps getArrayOps() { @Override public int readInto(A tgt, int tgtOffset, long srcOffset, int length) throws IOException { + // FIXME Must ensure the ranges are locked while we read! + long start = srcOffset + offsetInRanges; long end = start + length; Range totalReadRange = Range.closedOpen(start, end); if (!ranges.encloses(totalReadRange)) { - RangeSet gaps = ranges.complement().subRangeSet(totalReadRange); - - throw new ReadOverGapException("Attempt to read over gaps at: " + gaps); + RangeSet gaps = RangeUtils.gaps(totalReadRange,ranges); + throw new ReadOverGapException("Attempt to read over gaps. Gaps: " + gaps + ", Requested range: " + totalReadRange + ", Available ranges: " + ranges); } int result = backingBuffer.readInto(tgt, tgtOffset, srcOffset, length); @@ -146,7 +146,7 @@ public void write(long offsetInBuffer, A arrayWithItemsOfTypeT, int arrOffset, i throw new RuntimeException("Attempt to write beyond buffer capacity"); } - // TODO Add debug mode: Check when writing to already known ranges + // XXX Add debug mode: Check when writing to already known ranges // Range writeRange = Range.closedOpen(start, end); backingBuffer.write(offsetInBuffer, arrayWithItemsOfTypeT, arrOffset, arrLength); diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/ReadOverGapException.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/ReadOverGapException.java index 2529532e6e7..14d57fc5c4e 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/ReadOverGapException.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/ReadOverGapException.java @@ -29,7 +29,6 @@ * Read operations should typically be scheduled w.r.t. available data, however * concurrent modifications may invalidate such schedules and re-scheduling based on this * exception is a simple way to react to such changes. - * */ public class ReadOverGapException extends IOException diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceAccessorImpl.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceAccessorImpl.java index d85bc9d6add..5d6159f44c3 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceAccessorImpl.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceAccessorImpl.java @@ -22,30 +22,40 @@ package org.apache.jena.sparql.service.enhancer.slice.impl; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; import java.util.Map.Entry; +import java.util.NavigableMap; +import java.util.TreeMap; import java.util.concurrent.ConcurrentNavigableMap; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.stream.Collectors; -import com.google.common.base.Preconditions; -import com.google.common.collect.*; -import com.google.common.primitives.Ints; - +import org.apache.jena.atlas.lib.Closeable; import org.apache.jena.sparql.service.enhancer.claimingcache.Ref; import org.apache.jena.sparql.service.enhancer.claimingcache.RefFuture; +import org.apache.jena.sparql.service.enhancer.concurrent.AutoLock; import org.apache.jena.sparql.service.enhancer.impl.util.AutoCloseableWithLeakDetectionBase; import org.apache.jena.sparql.service.enhancer.impl.util.FinallyRunAll; import org.apache.jena.sparql.service.enhancer.impl.util.PageUtils; -import org.apache.jena.sparql.service.enhancer.slice.api.Disposable; import org.apache.jena.sparql.service.enhancer.slice.api.Slice; import org.apache.jena.sparql.service.enhancer.slice.api.SliceAccessor; import org.apache.jena.sparql.service.enhancer.slice.api.SliceWithPages; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.base.Preconditions; +import com.google.common.collect.ContiguousSet; +import com.google.common.collect.DiscreteDomain; +import com.google.common.collect.Range; +import com.google.common.collect.RangeMap; +import com.google.common.collect.RangeSet; +import com.google.common.collect.TreeRangeMap; +import com.google.common.primitives.Ints; + /** * A sequence of claimed ranges within a certain range, whereas the range * can be modified resulting in an incremental change of the claims. @@ -65,18 +75,11 @@ public class SliceAccessorImpl { private static final Logger logger = LoggerFactory.getLogger(SliceAccessorImpl.class); - // protected SmartRangeCacheImpl cache; protected SliceWithPages slice; - protected Range offsetRange; protected ConcurrentNavigableMap>> claimedPages = new ConcurrentSkipListMap<>(); - // protected NavigableMap> pageMap; - protected boolean isLocked = false; - - /** The number of items to process in one batch (before checking for conditions such as interrupts or no-more-demand) */ - protected int bulkSize = 16; - - protected Collection evictionGuards = new ArrayList<>(); + // protected boolean isLocked = false; + protected Collection evictionGuards = new ArrayList<>(); public SliceAccessorImpl(SliceWithPages cache) { super(); @@ -110,25 +113,25 @@ public void claimByOffsetRange(long startOffset, long endOffset) { claimByPageIdRange(startPageId, endPageId); } - protected synchronized void claimByPageIdRange(long startPageId, long endPageId) { + protected void claimByPageIdRange(long startPageId, long endPageId) { ensureOpen(); - ensureUnlocked(); + // ensureUnlocked(); // Remove any claimed page before startPageId - NavigableMap>> prefixPagesToRelease = claimedPages.headMap(startPageId, false); - prefixPagesToRelease.values().forEach(Ref::close); - prefixPagesToRelease.clear(); + NavigableMap>> headPagesToRelease = claimedPages.headMap(startPageId, false); + headPagesToRelease.values().forEach(Ref::close); + headPagesToRelease.clear(); // Remove any claimed page after endPageId - NavigableMap>> suffixPagesToRelease = claimedPages.tailMap(endPageId, false); - suffixPagesToRelease.values().forEach(Ref::close); - suffixPagesToRelease.clear(); + NavigableMap>> tailPagesToRelease = claimedPages.tailMap(endPageId, false); + tailPagesToRelease.values().forEach(Ref::close); + tailPagesToRelease.clear(); // Phase 1/2: Trigger loading of all pages for (long i = startPageId; i <= endPageId; ++i) { claimedPages.computeIfAbsent(i, idx -> { if (logger.isTraceEnabled()) { - logger.trace("Acquired page item [" + idx + "]"); + logger.trace("Acquired page item [{}]", idx); } RefFuture> page = slice.getPageForPageId(idx); @@ -164,31 +167,31 @@ protected NavigableMap> computePageMap() { TreeMap::new)); } - protected void ensureUnlocked() { - if (isLocked) { - throw new IllegalStateException("Pages ware already locked - need to be unlocked first"); - } - } - - @Override - public void lock() { - ensureUnlocked(); - - isLocked = true; - // updatePageMap(); - // pageMap.values().forEach(page -> page.getReadWriteLock().readLock().lock()); - // Prevent creation of new executors (other than by us) while we analyze the state - // slice.getWorkerCreationLock().lock(); - } - - @Override - public void unlock() { - // Unlock all pages - // updatePageMap(); - // pageMap.values().forEach(page -> page.getReadWriteLock().readLock().unlock()); - // slice.getWorkerCreationLock().unlock(); - isLocked = false; - } +// protected void ensureUnlocked() { +// if (isLocked) { +// throw new IllegalStateException("Pages were already locked - need to be unlocked first"); +// } +// } + +// @Override +// public void lock() { +// ensureUnlocked(); +// +// isLocked = true; +// // updatePageMap(); +// // pageMap.values().forEach(page -> page.getReadWriteLock().readLock().lock()); +// // Prevent creation of new executors (other than by us) while we analyze the state +// // slice.getWorkerCreationLock().lock(); +// } + +// @Override +// public void unlock() { +// // Unlock all pages +// // updatePageMap(); +// // pageMap.values().forEach(page -> page.getReadWriteLock().readLock().unlock()); +// // slice.getWorkerCreationLock().unlock(); +// isLocked = false; +// } public void releaseEvictionGuards() { if (!evictionGuards.isEmpty()) { @@ -201,9 +204,9 @@ public void releaseEvictionGuards() { @Override public void releaseAll() { - if (isLocked) { - unlock(); - } +// if (isLocked) { +// unlock(); +// } // Release all claimed pages // Remove all claimed pages before the checkpoint @@ -218,7 +221,7 @@ public void releaseAll() { } @Override - public synchronized void write(long offset, A arrayWithItemsOfTypeT, int arrOffset, int arrLength) { + public void write(long offset, A arrayWithItemsOfTypeT, int arrOffset, int arrLength) { ensureOpen(); @@ -230,9 +233,7 @@ public synchronized void write(long offset, A arrayWithItemsOfTypeT, int arrOffs long nextOffset = offset; int nextArrOffset = arrOffset; - Lock lock = slice.getReadWriteLock().writeLock(); - lock.lock(); - try { + try (AutoLock lock = AutoLock.lock(slice.getReadWriteLock().writeLock())) { long knownSize = slice.getKnownSize(); int remaining = arrLength; @@ -254,16 +255,17 @@ public synchronized void write(long offset, A arrayWithItemsOfTypeT, int arrOffs numItemsUntilPageKnownSize)), remaining); - Lock contentWriteLock = buffer.getReadWriteLock().writeLock(); - contentWriteLock.lock(); +// Lock contentWriteLock = buffer.getReadWriteLock().writeLock(); +// contentWriteLock.lock(); try { buffer.getRangeBuffer().write(offsetInPage, arrayWithItemsOfTypeT, nextArrOffset, limit); } catch (IOException e) { throw new RuntimeException(e); - } finally { - contentWriteLock.unlock(); } +// } finally { +// contentWriteLock.unlock(); +// } remaining -= limit; nextOffset += limit; nextArrOffset += limit; @@ -272,16 +274,19 @@ public synchronized void write(long offset, A arrayWithItemsOfTypeT, int arrOffs slice.updateMinimumKnownSize(nextOffset); slice.getLoadedRanges().add(totalWriteRange); slice.getHasDataCondition().signalAll(); - - } finally { - lock.unlock(); } } - - /** Read a range of data - does not await any new data */ @Override public int unsafeRead(A tgt, int tgtOffset, long srcOffset, int length) throws IOException { + // M must prevent any changes to the slice metadata while reading! + try (AutoLock autoLock = AutoLock.lock(getSlice().getReadWriteLock().readLock())) { + return unsafeReadInternal(tgt, tgtOffset, srcOffset, length); + } + } + + /** Read a range of data - does not await any new data. Used to read cached ranges. */ + public int unsafeReadInternal(A tgt, int tgtOffset, long srcOffset, int length) throws IOException { ensureOpen(); Range totalReadRange = Range.closedOpen(srcOffset, srcOffset + length); @@ -320,12 +325,13 @@ public int unsafeRead(A tgt, int tgtOffset, long srcOffset, int length) throws I } /** - * Method is subject to removal - use sequentialReaderForSlice.read + * Method is subject to removal - use {@link #unsafeRead(Object, int, long, int)}. * * The range [srcOffset, srcOffset + length) must be within the claimed range! * @throws IOException * */ + // @Override public int blockingRead(A tgt, int tgtOffset, long srcOffset, int length) throws IOException { ensureOpen(); @@ -334,13 +340,12 @@ public int blockingRead(A tgt, int tgtOffset, long srcOffset, int length) throws offsetRange.encloses(totalReadRange), "Read range " + totalReadRange + " is not enclosed by claimed range " + offsetRange); - int result; - long currentOffset = srcOffset; - ReadWriteLock rwl = slice.getReadWriteLock(); - Lock readLock = rwl.readLock(); + ReadWriteLock sliceReadWriteLock = slice.getReadWriteLock(); + Lock readLock = sliceReadWriteLock.readLock(); readLock.lock(); + long pageSize = slice.getPageSize(); try { RangeSet loadedRanges = slice.getLoadedRanges(); @@ -370,7 +375,7 @@ public int blockingRead(A tgt, int tgtOffset, long srcOffset, int length) throws // Wait for data to become available // Solution based on https://stackoverflow.com/questions/13088363/how-to-wait-for-data-with-reentrantreadwritelock - Lock writeLock = rwl.writeLock(); + Lock writeLock = sliceReadWriteLock.writeLock(); readLock.unlock(); writeLock.lock(); @@ -379,7 +384,9 @@ public int blockingRead(A tgt, int tgtOffset, long srcOffset, int length) throws while ((entry = loadedRanges.rangeContaining(currentOffset)) == null && ((knownSize = slice.getMaximumKnownSize()) < 0 || currentOffset < knownSize)) { try { - logger.info("Awaiting more data: " + entry + " " + currentOffset + " " + knownSize); + if (logger.isDebugEnabled()) { + logger.debug("Awaiting more data: entry: {}, currentOffset: {}, knownSize: {}", entry, currentOffset, knownSize); + } slice.getHasDataCondition().await(); } catch (InterruptedException e) { throw new RuntimeException(e); @@ -393,6 +400,7 @@ public int blockingRead(A tgt, int tgtOffset, long srcOffset, int length) throws } } finally { readLock.unlock(); + readLock = null; } if (failures != null && !failures.isEmpty()) { @@ -400,7 +408,6 @@ public int blockingRead(A tgt, int tgtOffset, long srcOffset, int length) throws failures.get(0)); } - if (entry == null) { close(); result = -1; // We were positioned at or past the end of data so there was nothing to read @@ -414,27 +421,30 @@ public int blockingRead(A tgt, int tgtOffset, long srcOffset, int length) throws long startAbs = cset.first(); long endAbs = startAbs + result; - long pageSize = slice.getPageSize(); long startPageId = PageUtils.getPageIndexForOffset(startAbs, pageSize); long endPageId = PageUtils.getPageIndexForOffset(endAbs, pageSize); long indexInPage = PageUtils.getIndexInPage(startAbs, pageSize); + int nextTgtOffset = tgtOffset; for (long i = startPageId; i <= endPageId; ++i) { - long endIndex = i == endPageId + long endIndexRaw = i == endPageId ? PageUtils.getIndexInPage(endAbs, pageSize) : pageSize; + int endIndex = Math.toIntExact(endIndexRaw); RefFuture> currentPageRef = getClaimedPages().get(i); BufferView buffer = currentPageRef.await(); - buffer.getRangeBuffer().readInto(tgt, tgtOffset, indexInPage, Math.toIntExact(endIndex)); - + buffer.getRangeBuffer().readInto(tgt, nextTgtOffset, indexInPage, endIndex); + nextTgtOffset += endIndex; indexInPage = 0; } } } finally { - readLock.unlock(); + if (readLock != null) { + readLock.unlock(); + } } @@ -449,10 +459,9 @@ protected void closeActual() { @Override public void addEvictionGuard(RangeSet ranges) { - Disposable disposable = slice.addEvictionGuard(ranges); + Closeable disposable = slice.addEvictionGuard(ranges); if (disposable != null) { evictionGuards.add(disposable); } } - } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceBase.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceBase.java index 6d8b3d5258c..6557aa9cb59 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceBase.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceBase.java @@ -19,6 +19,7 @@ * SPDX-License-Identifier: Apache-2.0 */ + package org.apache.jena.sparql.service.enhancer.slice.impl; import java.util.List; @@ -26,16 +27,16 @@ import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import com.google.common.collect.RangeMap; -import com.google.common.collect.RangeSet; import org.apache.jena.sparql.service.enhancer.slice.api.ArrayOps; import org.apache.jena.sparql.service.enhancer.slice.api.Slice; import org.apache.jena.sparql.service.enhancer.slice.api.SliceMetaDataBasic; +import com.google.common.collect.RangeMap; +import com.google.common.collect.RangeSet; + public abstract class SliceBase implements Slice { - protected ArrayOps arrayOps; // A read/write lock for synchronizing reads/writes to the slice @@ -51,7 +52,6 @@ public SliceBase(ArrayOps arrayOps) { protected abstract SliceMetaDataBasic getMetaData(); - @Override public RangeSet getLoadedRanges() { return getMetaData().getLoadedRanges(); diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceInMemoryCache.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceInMemoryCache.java index c5864fcb3c8..365117812ff 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceInMemoryCache.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceInMemoryCache.java @@ -19,28 +19,33 @@ * SPDX-License-Identifier: Apache-2.0 */ + package org.apache.jena.sparql.service.enhancer.slice.impl; -import java.util.Set; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; -import com.google.common.cache.CacheBuilder; -import com.google.common.collect.Range; -import com.google.common.collect.RangeSet; +import org.apache.jena.atlas.lib.Closeable; import org.apache.jena.sparql.service.enhancer.claimingcache.AsyncClaimingCache; -import org.apache.jena.sparql.service.enhancer.claimingcache.AsyncClaimingCacheImplGuava; +import org.apache.jena.sparql.service.enhancer.claimingcache.AsyncClaimingCacheImplCaffeine; +import org.apache.jena.sparql.service.enhancer.claimingcache.PredicateRangeSet; +import org.apache.jena.sparql.service.enhancer.claimingcache.PredicateTrue; import org.apache.jena.sparql.service.enhancer.claimingcache.RefFuture; -import org.apache.jena.sparql.service.enhancer.impl.util.LockUtils; +import org.apache.jena.sparql.service.enhancer.concurrent.AutoLock; +import org.apache.jena.sparql.service.enhancer.concurrent.LockWrapper; +import org.apache.jena.sparql.service.enhancer.concurrent.ReadWriteLockModular; import org.apache.jena.sparql.service.enhancer.impl.util.PageUtils; import org.apache.jena.sparql.service.enhancer.slice.api.ArrayOps; -import org.apache.jena.sparql.service.enhancer.slice.api.Disposable; import org.apache.jena.sparql.service.enhancer.slice.api.Slice; import org.apache.jena.sparql.service.enhancer.slice.api.SliceMetaDataBasic; import org.apache.jena.sparql.service.enhancer.slice.api.SliceWithPages; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; + /** * A slice implementation that starts to discard pages once there are too many. */ @@ -53,37 +58,43 @@ public class SliceInMemoryCache protected SliceMetaDataWithPages metaData; protected AsyncClaimingCache> pageCache; - protected SliceInMemoryCache(ArrayOps arrayOps, int pageSize, AsyncClaimingCacheImplGuava.Builder> cacheBuilder) { + protected SliceInMemoryCache(ArrayOps arrayOps, int pageSize, AsyncClaimingCacheImplCaffeine.Builder> cacheBuilder) { super(arrayOps); this.metaData = new SliceMetaDataWithPagesImpl(pageSize); this.pageCache = cacheBuilder .setCacheLoader(this::loadPage) - .setAtomicRemovalListener(n -> evictPage(n.getKey())) + .setAtomicRemovalListener((k, v, c) -> evictPage(k)) .build(); } public static Slice create(ArrayOps arrayOps, int pageSize, int maxCachedPages) { - AsyncClaimingCacheImplGuava.Builder> cacheBuilder = AsyncClaimingCacheImplGuava.newBuilder( - CacheBuilder.newBuilder().maximumSize(maxCachedPages)); - + AsyncClaimingCacheImplCaffeine.Builder> cacheBuilder = AsyncClaimingCacheImplCaffeine.newBuilder( + Caffeine.newBuilder().maximumSize(maxCachedPages)); return new SliceInMemoryCache<>(arrayOps, pageSize, cacheBuilder); } + // FIXME This looks wrong: Eviction must consider eviction guards. + /** + * This method is called after eviction guard checks. + * Locking the slice first registers an eviction guard before actually locking the slice. + */ protected void evictPage(long pageId) { long pageOffset = getPageOffsetForPageId(pageId); int pageSize = metaData.getPageSize(); Range pageRange = Range.closedOpen(pageOffset, pageOffset + pageSize); if (logger.isDebugEnabled()) { - logger.debug("Attempting to evict page " + pageId + " with range " + pageRange); + logger.debug("Attempting to evict page {} with range {}.", pageId, pageRange); } - LockUtils.runWithLock(readWriteLock.writeLock(), () -> { - metaData.getLoadedRanges().remove(pageRange); - }); + + // The public locking mechanism registers an eviction guard before acquisition of the internal lock + // So eviction cannot happen if the slice is locked + + metaData.getLoadedRanges().remove(pageRange); + if (logger.isDebugEnabled()) { - logger.debug("Evicted page " + pageId + " with range " + pageRange); + logger.debug("Evicted page {} with range {}.", pageId, pageRange); } - } protected BufferView loadPage(long pageId) { @@ -105,12 +116,109 @@ public long getGeneration() { @Override public ReadWriteLock getReadWriteLock() { - return readWriteLock; + // return readWriteLock; + return SliceInMemoryCache.this.getReadWriteLock(); + } + + @Override + public String toString() { + // return "(BufferView pageId " + pageId + " " + metaData + ")"; + return "(BufferView pageId " + pageId + ")"; } }; return result; } + /** LockWrapper that disables eviction before actually acquiring the lock. */ + protected class EvictionGuardedLock + extends LockWrapper { + + protected Lock delegate; + protected Closeable disposable; + + public EvictionGuardedLock(Lock delegate) { + super(); + this.delegate = delegate; + } + + @Override + protected Lock getDelegate() { + return delegate; + } + + protected void disableEviction() { + if (disposable != null) { + throw new IllegalStateException("Lock is already held"); + } + disposable = pageCache.addEvictionGuard(PredicateTrue.get()); + } + + protected void enableEviction() { + if (disposable == null) { + throw new IllegalStateException("Lock is not held"); + } + disposable.close(); + disposable = null; + } + +// @Override +// public void lock() { +// super.lock(); +// customLock(); +// } +// +// @Override +// public void lockInterruptibly() throws InterruptedException { +// super.lockInterruptibly(); +// customLock(); +// } +// +// @Override +// public void unlock() { +// customUnlock(); +// super.unlock(); +// } + + @Override + public void lock() { + disableEviction(); + try { + super.lock(); + } catch (Throwable t) { + enableEviction(); + t.addSuppressed(new RuntimeException()); + throw t; + } + } + + @Override + public void lockInterruptibly() throws InterruptedException { + disableEviction(); + try { + super.lockInterruptibly(); + } catch (Throwable t) { + enableEviction(); + t.addSuppressed(new RuntimeException()); + throw t; + } + } + + @Override + public void unlock() { + try { + super.unlock(); + } finally { + enableEviction(); + } + } + } + + @Override + public ReadWriteLock getReadWriteLock() { + ReadWriteLock rwl = super.getReadWriteLock(); + return new ReadWriteLockModular(new EvictionGuardedLock(rwl.readLock()), new EvictionGuardedLock(rwl.writeLock())); + } + @Override protected SliceMetaDataBasic getMetaData() { return metaData; @@ -132,18 +240,19 @@ public RefFuture> getPageForPageId(long pageId) { } @Override - public Disposable addEvictionGuard(RangeSet ranges) { + public Closeable addEvictionGuard(RangeSet ranges) { long pageSize = getPageSize(); - Set pageIds = PageUtils.touchedPageIndices(ranges.asRanges(), pageSize); + RangeSet pageIdRanges = PageUtils.touchedPageIndexRangeSet(ranges.asRanges(), pageSize); if (logger.isDebugEnabled()) { - logger.debug("Added eviction guard over ranges " + ranges + " affecting page ids " + pageIds); + logger.debug("Added eviction guard over page id ranges {}.", pageIdRanges); } - Disposable core = pageCache.addEvictionGuard(key -> pageIds.contains(key)); + PredicateRangeSet rangeSetMatcher = new PredicateRangeSet<>(pageIdRanges); + Closeable core = pageCache.addEvictionGuard(rangeSetMatcher); return () -> { if (logger.isDebugEnabled()) { - logger.debug("Removed eviction guard over ranges " + ranges + " affecting page ids " + pageIds); + logger.debug("Added eviction guard over page id ranges {}.", pageIdRanges); } core.close(); }; @@ -151,17 +260,17 @@ public Disposable addEvictionGuard(RangeSet ranges) { @Override public void clear() { - ReadWriteLock rwl = getReadWriteLock(); - Lock writeLock = rwl.writeLock(); - writeLock.lock(); - try { + try (AutoLock lock = AutoLock.lock(getReadWriteLock().writeLock())) { pageCache.invalidateAll(); setMinimumKnownSize(0); setMaximumKnownSize(Long.MAX_VALUE); getFailedRanges().clear(); getLoadedRanges().clear(); - } finally { - writeLock.unlock(); } } + + @Override + public String toString() { + return "SliceInMemoryCache " + metaData; + } } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceMetaDataImpl.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceMetaDataImpl.java index fefdf49b21c..cda08ad3764 100644 --- a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceMetaDataImpl.java +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/slice/impl/SliceMetaDataImpl.java @@ -19,16 +19,18 @@ * SPDX-License-Identifier: Apache-2.0 */ + package org.apache.jena.sparql.service.enhancer.slice.impl; import java.io.Serializable; import java.util.List; +import org.apache.jena.sparql.service.enhancer.slice.api.SliceMetaDataBasic; + import com.google.common.collect.RangeMap; import com.google.common.collect.RangeSet; import com.google.common.collect.TreeRangeMap; import com.google.common.collect.TreeRangeSet; -import org.apache.jena.sparql.service.enhancer.slice.api.SliceMetaDataBasic; public class SliceMetaDataImpl implements SliceMetaDataBasic, Serializable @@ -46,10 +48,10 @@ public class SliceMetaDataImpl public SliceMetaDataImpl() { this( - TreeRangeSet.create(), - TreeRangeMap.create(), - 0, - Long.MAX_VALUE + TreeRangeSet.create(), + TreeRangeMap.create(), + 0, + Long.MAX_VALUE ); } diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/util/IdPool.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/util/IdPool.java new file mode 100644 index 00000000000..37a491aeca0 --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/util/IdPool.java @@ -0,0 +1,87 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +package org.apache.jena.sparql.service.enhancer.util; + +import java.util.Iterator; +import java.util.TreeSet; + +/** + * A synchronized pool of integer ids. + * Acquired ids must be eventually free'd using {@link #giveBack(int)}. + */ +public class IdPool { + private volatile int i = 0; + private final TreeSet ids = new TreeSet<>(); + + /** Allocate and return an unused id. */ + public synchronized int acquire() { + int result; + if (!ids.isEmpty()) { + Iterator it = ids.iterator(); + result = it.next(); + it.remove(); + } else { + result = i++; + } + return result; + } + + /** Return the size of recycled ids. */ + public int getRecyclePoolSize() { + return ids.size(); + } + + /** + * Return an id to the pool of available ids. + * + * @implNote If the highest acquired is returned then all consecutive + * trailing ids up to (excluding) the returned id are removed from the pool. + * In the worst case the will be overhead for completely emptying the pool. + */ + public synchronized void giveBack(int v) { + if (v >= i) { + throw new IllegalArgumentException("Attempt to give back a value " + v + " which is greater than the largest generated one " + i + "."); + } + + if (v + 1 == i) { + // Case where the highest acquired value is returned + --i; + Iterator it = ids.descendingIterator(); + while (it.hasNext()) { + int id = it.next(); + if (id + 1 == i) { + it.remove(); + --i; + } else { + break; + } + } + } else { + if (ids.contains(v)) { + throw new IllegalArgumentException("The value has already been given back: " + v); + } + + ids.add(v); + } + } +} diff --git a/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/util/LinkedList.java b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/util/LinkedList.java new file mode 100644 index 00000000000..dd5bb7c7ebd --- /dev/null +++ b/jena-serviceenhancer/src/main/java/org/apache/jena/sparql/service/enhancer/util/LinkedList.java @@ -0,0 +1,405 @@ +/* + * 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 + * + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + */ + + +package org.apache.jena.sparql.service.enhancer.util; + +import java.io.Serializable; +import java.util.AbstractList; +import java.util.Collection; +import java.util.Iterator; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.Objects; + +/** + * A doubly linked list for keeping track of a sequence of items, + * with O(1) insertion and deletion (using {@link LinkedListNode#unlink()}). + * Nodes are created with {@link #newNode()} and are owned by the creating list. + * + * Use {@link #append(Object)} to add an item at the end of the list and obtain a corresponding {@link LinkedListNode} instance. + * Use {@link LinkedListNode#unlink()} to remove a specific node from the list + * and {@link LinkedListNode#moveToEnd()} to (re-)link a node as the last item of the list. + * + * The list is not thread-safe. + */ +public class LinkedList + extends AbstractList + implements Serializable +{ + private static final long serialVersionUID = 1L; + + public static class LinkedListNode + implements Serializable + { + private static final long serialVersionUID = 1L; + + private volatile T value; + private volatile LinkedListNode prev; + private volatile LinkedListNode next; + private final LinkedList list; + + private LinkedListNode(LinkedList list) { + this(list, null); + } + private LinkedListNode(LinkedList list,T value) { + super(); + this.list = list; + this.value = value; + } + public T getValue() { + return value; + } + public void setValue(T value) { + this.value = value; + } + public LinkedListNode getPrev() { + return prev; + } + public LinkedListNode getNext() { + return next; + } + public LinkedList getList() { + return list; + } + /** + * A node is linked if either: + *
    + *
  • prev or next are non-null
  • + *
  • the node is referenced by list.first
  • + *
+ * + * Conversely, a node is unlinked if prev and next are both null, and + * this node is not referenced by list.first. + */ + public boolean isLinked() { + return prev != null || next != null || list.first == this; + } + public void unlink() { + list.unlink(this); + } + public void moveToEnd() { + list.moveToEnd(this); + } + @Override + public String toString() { + return "LinkedListNode [value=" + value + "]"; + } + } + + private volatile LinkedListNode first; + private volatile LinkedListNode last; + private volatile int size; + + public LinkedList() { + super(); + } + + public LinkedList(Collection items) { + super(); + items.forEach(this::append); + } + + public LinkedListNode getFirstNode() { + return first; + } + + public LinkedListNode getLastNode() { + return last; + } + + /** Create a new unlinked node. The node can only be inserted into this list. */ + public LinkedListNode newNode() { + return newNode(null); + } + + public LinkedListNode newNode(T value) { + return new LinkedListNode<>(this, value); + } + + /** Use {@link #append(Object)} to add a value and obtain its linked list node. */ + @Override + public boolean add(T value) { + append(value); + return true; + } + + public LinkedListNode append(T value) { + LinkedListNode result = newNode(); + result.value = value; + moveToEnd(result); + return result; + } + + public void moveToEnd(LinkedListNode node) { + unlink(node); + if (first == null) { + first = node; + } else { + last.next = node; + node.prev = last; + } + last = node; + ++size; + } + + /** Add node after the insert point. If the insert point is null then the node becomes first. */ + public void addAfter(LinkedListNode insertPoint, LinkedListNode node) { + checkOwner(insertPoint); + if (insertPoint == null) { + if (first == null) { + // Insert as first + checkOwner(node); // node cannot be linked - if it was then first would not be null + first = node; + last = node; + } else { + // Insert before first + unlink(node); + LinkedListNode tmp = first; + tmp.prev = node; + node.next = tmp; + first = node; + } + } else { + if (insertPoint.next != node) { + unlink(node); + + LinkedListNode tmp = insertPoint.next; + insertPoint.next = node; + node.prev = insertPoint; + + if (tmp != null) { + tmp.prev = node; + node.next = tmp; + } + + if (last == insertPoint) { + last = node; + } + } + } + } + + private void checkOwner(LinkedListNode node) { + if (node.list != this) { + throw new IllegalArgumentException("Cannot unlink a node that does not belong to this list."); + } + } + + private void unlink(LinkedListNode node) { + checkOwner(node); + if (node.isLinked()) { + if (node == first) { + first = node.next; + } + if (node == last) { + last = node.prev; + } + if (node.prev != null) { + node.prev.next = node.next; + } + if (node.next != null) { + node.next.prev = node.prev; + } + node.prev = null; + node.next = null; + --size; + } + } + + @Override + public int size() { + return size; + } + + private LinkedListNode findNode(int index) { + int s = size(); + if (index < 0 || index >= s) { + throw new IndexOutOfBoundsException(); + } + int halfSize = s >> 1; + LinkedListNode node; + int i; + if (index <= halfSize) { + node = this.first; + for (i = 0; i < index; ++i) { + node = node.next; + } + } else { + node = this.last; + for (i = s - 1; i > index; --i) { + node = node.prev; + } + } + return node; + } + + @Override + public T get(int index) { + LinkedListNode node = findNode(index); + return node.value; + } + + @Override + public ListIterator listIterator(int index) { + ListIterator result; + int s = size(); + if (index == s) { // Special case to position after the last element + LinkedListNode node = getLastNode(); + result = new LinkedListIterator(node, s, false); + } else { + LinkedListNode node = findNode(index); + result = new LinkedListIterator(node, index, true); + } + return result; + } + + @Override + public Iterator iterator() { + return listIterator(0); + } + + protected class LinkedListIterator + implements ListIterator { + + // The pointer to the last returned node - null if absent. + protected LinkedListNode removable; + + // The next node to be returned by next() + protected LinkedListNode current; + protected boolean isForward; + protected int currentIndex; + + public LinkedListIterator(LinkedListNode current, int currentIndex, boolean isForward) { + super(); + this.current = current; + this.isForward = isForward; + this.currentIndex = currentIndex; + } + + protected void ensureValid(LinkedListNode node) { + Objects.requireNonNull(node); + if (!node.isLinked()) { + throw new IllegalStateException("Linked list iterator points to an unlinked node."); + } + } + + @Override + public boolean hasNext() { + return current != null && (isForward || current.getNext() != null); + } + + private void prepareForwardStep() { + if (!isForward) { + ensureValid(current); + LinkedListNode next = current.getNext(); + if (next != null) { + current = next; + } else { + throw new NoSuchElementException(); + } + isForward = true; + } + } + + private void prepareBackwardStep() { + if (isForward) { + ensureValid(current); + LinkedListNode prev = current.getPrev(); + if (prev != null) { + current = prev; + } else { + throw new NoSuchElementException(); + } + isForward = false; + } + } + + @Override + public T next() { + prepareForwardStep(); + ensureValid(current); + removable = current; + LinkedListNode next = current.getNext(); + if (next != null) { + current = next; + } else { + isForward = false; + } + ++currentIndex; + return removable.getValue(); + } + + @Override + public void remove() { + Objects.requireNonNull(removable, "Linked list iterator is not positioned at a removable element."); + unlink(removable); + removable = null; + } + + @Override + public boolean hasPrevious() { + return current != null && (!isForward || current.getPrev() != null); + } + + @Override + public T previous() { + prepareBackwardStep(); + ensureValid(current); + removable = current; + LinkedListNode prev = current.getPrev(); + if (prev != null) { + current = prev; + } else { + isForward = true; + } + --currentIndex; + return removable.getValue(); + } + + @Override + public int nextIndex() { + return currentIndex; + } + + @Override + public int previousIndex() { + return currentIndex - 1; + } + + @Override + public void set(T e) { + ensureValid(current); + current.setValue(e); + } + + @Override + public void add(T e) { + prepareForwardStep(); + ensureValid(current); + LinkedListNode node = newNode(e); + LinkedListNode insertPoint = current.getPrev(); + addAfter(insertPoint, node); + current = node; + removable = null; + } + } +} diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/assembler/TestServiceEnhancerDatasetAssembler.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/assembler/TestServiceEnhancerDatasetAssembler.java index 66bffb8ca43..f15719ec34c 100644 --- a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/assembler/TestServiceEnhancerDatasetAssembler.java +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/assembler/TestServiceEnhancerDatasetAssembler.java @@ -7,20 +7,20 @@ * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * 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. - * - * SPDX-License-Identifier: Apache-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.jena.sparql.service.enhancer.assembler; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + import java.io.File; import java.io.IOException; import java.io.StringReader; @@ -28,6 +28,8 @@ import java.nio.file.Path; import java.util.Comparator; +import org.junit.jupiter.api.Test; + import org.apache.jena.query.Dataset; import org.apache.jena.query.DatasetFactory; import org.apache.jena.query.QueryExecException; @@ -45,21 +47,21 @@ import org.apache.jena.system.Txn; import org.apache.jena.tdb2.assembler.VocabTDB2; import org.apache.jena.vocabulary.RDF; -import org.junit.Assert; -import org.junit.Test; - public class TestServiceEnhancerDatasetAssembler { private static final String SPEC_STR_01 = String.join("\n", "PREFIX ja: ", "PREFIX se: ", - " a se:DatasetServiceEnhancer ; ja:baseDataset .", + " a se:DatasetServiceEnhancer ; ja:dataset .", " se:cacheMaxEntryCount 5 ; se:cachePageSize 1000 ; se:cacheMaxPageCount 10 .", " se:bulkMaxSize 20 ; se:bulkSize 10 ; se:bulkMaxOutOfBandSize 5 .", " a ja:MemoryDataset ." ); + // Replace the property property "ja:dataset" with the legacy one. + private static final String SPEC_STR_01_LEGACY = SPEC_STR_01.replace("ja:dataset", "ja:baseDataset"); + /** * This test case attempts to assemble a dataset with the service enhancer plugin * set up in its context. A query making use of enhancer features is fired against it. @@ -67,15 +69,27 @@ public class TestServiceEnhancerDatasetAssembler */ @Test public void testAssembler() { + testAssemblerActual(SPEC_STR_01); + } + + /** + * Same as {@link #testAssembler()} but uses the incorrect ja:baseDataset property. + */ + // @Test // Disabled by default because it generates a deprecation warning. + public void testAssembler_legacy() { + testAssemblerActual(SPEC_STR_01_LEGACY); + } + + private void testAssemblerActual(String specStr) { Model spec = ModelFactory.createDefaultModel(); - RDFDataMgr.read(spec, new StringReader(SPEC_STR_01), null, Lang.TURTLE); + RDFDataMgr.read(spec, new StringReader(specStr), null, Lang.TURTLE); Dataset dataset = DatasetFactory.assemble(spec.getResource("urn:example:root")); Context cxt = dataset.getContext(); - Assert.assertEquals(20, cxt.getInt(ServiceEnhancerConstants.serviceBulkMaxBindingCount, -1)); - Assert.assertEquals(10, cxt.getInt(ServiceEnhancerConstants.serviceBulkBindingCount, -1)); - Assert.assertEquals(5, cxt.getInt(ServiceEnhancerConstants.serviceBulkMaxOutOfBandBindingCount, -1)); + assertEquals(20, cxt.getInt(ServiceEnhancerConstants.serviceBulkMaxBindingCount, -1)); + assertEquals(10, cxt.getInt(ServiceEnhancerConstants.serviceBulkBindingCount, -1)); + assertEquals(5, cxt.getInt(ServiceEnhancerConstants.serviceBulkMaxOutOfBandBindingCount, -1)); try (QueryExecution qe = QueryExecutionFactory.create( "SELECT * { BIND( AS ?x) SERVICE { ?x ?y ?z } }", dataset)) { @@ -84,22 +98,24 @@ public void testAssembler() { } /** Test that calling cacheRm fails because enableMgmt has not been set to true in the context */ - @Test(expected = QueryExecException.class) + @Test public void testAssemblerMgmtFail() { String specStr = String.join("\n", "PREFIX ja: ", "PREFIX se: ", - " a se:DatasetServiceEnhancer ; ja:baseDataset .", + " a se:DatasetServiceEnhancer ; ja:dataset .", " a ja:MemoryDataset ." ); Model spec = ModelFactory.createDefaultModel(); RDFDataMgr.read(spec, new StringReader(specStr), null, Lang.TURTLE); Dataset dataset = DatasetFactory.assemble(spec.getResource("urn:example:root")); - try (QueryExecution qe = QueryExecutionFactory.create( - "PREFIX se: SELECT se:cacheRm(0) { }", dataset)) { - Assert.assertEquals(1, ResultSetFormatter.consume(qe.execSelect())); - } + assertThrows(QueryExecException.class, () -> { + try (QueryExecution qe = QueryExecutionFactory.create( + "PREFIX se: SELECT se:cacheRm(0) { }", dataset)) { + assertEquals(1, ResultSetFormatter.consume(qe.execSelect())); + } + }); } /** Test for cacheRm to execute successfully due to enableMgmt having been set to true in the context */ @@ -108,7 +124,7 @@ public void testAssemblerMgmtSuccess() { String specStr = String.join("\n", "PREFIX ja: ", "PREFIX se: ", - " a se:DatasetServiceEnhancer ; se:enableMgmt true ; ja:baseDataset .", + " a se:DatasetServiceEnhancer ; se:enableMgmt true ; ja:dataset .", " a ja:MemoryDataset ." ); @@ -120,12 +136,12 @@ public void testAssemblerMgmtSuccess() { dataset.asDatasetGraph().getDefaultGraph().add(RDF.Nodes.type, RDF.Nodes.type, RDF.Nodes.Property); try (QueryExecution qe = QueryExecutionFactory.create( "SELECT * { SERVICE { ?s ?p ?o } }", dataset)) { - Assert.assertEquals(1, ResultSetFormatter.consume(qe.execSelect())); + assertEquals(1, ResultSetFormatter.consume(qe.execSelect())); } try (QueryExecution qe = QueryExecutionFactory.create( "PREFIX se: SELECT se:cacheRm(0) { }", dataset)) { - Assert.assertEquals(1, ResultSetFormatter.consume(qe.execSelect())); + assertEquals(1, ResultSetFormatter.consume(qe.execSelect())); } } @@ -142,7 +158,7 @@ public void testAssemblerTdbUnionDefaultGraph() throws IOException { "PREFIX ja: ", "PREFIX se: ", "PREFIX tdb2: ", - " a se:DatasetServiceEnhancer ; ja:baseDataset .", + " a se:DatasetServiceEnhancer ; ja:dataset .", " se:cacheMaxEntryCount 5 ; se:cachePageSize 1000 ; se:cacheMaxPageCount 10 .", " se:bulkMaxSize 20 ; se:bulkSize 10 ; se:bulkMaxOutOfBandSize 5 .", " a tdb2:DatasetTDB2 .", @@ -173,7 +189,7 @@ public void testAssemblerTdbUnionDefaultGraph() throws IOException { } }); - Assert.assertEquals(4, actualRowCount); + assertEquals(4, actualRowCount); } finally { Files.walk(tdb2TmpFolder) .sorted(Comparator.reverseOrder()) diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/cache/TestAsyncClaimingCache.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/cache/TestAsyncClaimingCache.java new file mode 100644 index 00000000000..8e55c46ede4 --- /dev/null +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/cache/TestAsyncClaimingCache.java @@ -0,0 +1,66 @@ +/* + * 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.jena.sparql.service.enhancer.cache; + +import java.util.concurrent.TimeUnit; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.Scheduler; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.apache.jena.atlas.lib.Closeable; +import org.apache.jena.sparql.service.enhancer.claimingcache.AsyncClaimingCacheImplCaffeine; +import org.apache.jena.sparql.service.enhancer.claimingcache.RefFuture; + +public class TestAsyncClaimingCache { + @Test + @Disabled + public void test() throws InterruptedException { + + int maxCacheSize = 10; + + AsyncClaimingCacheImplCaffeine cache = AsyncClaimingCacheImplCaffeine.newBuilder( + Caffeine.newBuilder().maximumSize(maxCacheSize).expireAfterWrite(1, TimeUnit.MILLISECONDS).scheduler(Scheduler.systemScheduler())) + .setCacheLoader(key -> "Loaded " + key) + .setAtomicRemovalListener((k, v, c) -> System.out.println("Evicted " + k)) + .setClaimListener((k, v) -> System.out.println("Claimed: " + k)) + .setUnclaimListener((k, v) -> System.out.println("Unclaimed: " + k)) + .build(); + + RefFuture ref = cache.claim("hell"); + + Closeable disposable = cache.addEvictionGuard(k -> k.contains("hell")); + + System.out.println(ref.await()); + ref.close(); + + TimeUnit.SECONDS.sleep(5); + + RefFuture reclaim = cache.claim("hell"); + + disposable.close(); + + reclaim.close(); + + TimeUnit.SECONDS.sleep(5); + + System.out.println("done"); + } +} diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/concurrent/TestIdPool.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/concurrent/TestIdPool.java new file mode 100644 index 00000000000..87748da1da1 --- /dev/null +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/concurrent/TestIdPool.java @@ -0,0 +1,144 @@ +/* + * 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.jena.sparql.service.enhancer.concurrent; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ListIterator; + +import org.junit.jupiter.api.Test; + +import org.apache.jena.sparql.service.enhancer.util.IdPool; + +public class TestIdPool { + @Test + public void test01() { + IdPool pool = new IdPool(); + int id0 = pool.acquire(); + assertEquals(0, id0); + + int id1 = pool.acquire(); + assertEquals(1, id1); + + int id2 = pool.acquire(); + assertEquals(2, id2); + + // We have not given back any ids to the pool so no id can be recycled. + assertEquals(0, pool.getRecyclePoolSize()); + + // Give back and re-acquire ids starting from the lowest. + // Giving back a single id and immediately re-acquiring a new one should + // return the same id. + + pool.giveBack(id0); + id0 = pool.acquire(); + assertEquals(id0, 0); + + pool.giveBack(id1); + id1 = pool.acquire(); + assertEquals(id1, 1); + + pool.giveBack(id2); + id2 = pool.acquire(); + assertEquals(id2, 2); + + // We have re-acquired all ids so there shouldn't be any recycled ones. + assertEquals(0, pool.getRecyclePoolSize()); + + // Give back and re-acquire ids starting from the highest. + + pool.giveBack(id2); + id2 = pool.acquire(); + assertEquals(id2, 2); + + pool.giveBack(id1); + id1 = pool.acquire(); + assertEquals(id1, 1); + + pool.giveBack(id0); + id0 = pool.acquire(); + assertEquals(id0, 0); + + // Giving back all but the highest ids should track those ids in the recycle pool + pool.giveBack(id0); + pool.giveBack(id1); + + assertEquals(2, pool.getRecyclePoolSize()); + + // Giving back the highest id should now clear the recycle pool + pool.giveBack(id2); + assertEquals(0, pool.getRecyclePoolSize()); + + // Since all ids were given back then next acquired one should be 0 again + id0 = pool.acquire(); + assertEquals(0, id0); + } + + @Test + public void test_invalid_giveback_01() { + IdPool pool = new IdPool(); + int id0 = pool.acquire(); + assertEquals(0, id0); + + pool.giveBack(id0); + assertThrows(IllegalArgumentException.class, () -> { + pool.giveBack(id0); + }); + } + + @Test + public void test_random_01() { + IdPool pool = new IdPool(); + List ids = new ArrayList<>(); + + for (int j = 0; j < 20; ++j) { + // Acquire n=10 ids and make sure there are no duplicates + for (int i = 0; i < 10; ++i) { + int x = pool.acquire(); + if (ids.contains(x)) { + throw new RuntimeException("Error - got an id that is already in use"); + } + ids.add(x); + } + + // Shuffle the ids and give back the last m=8 ids + Collections.shuffle(ids); + ListIterator it = ids.listIterator(ids.size()); + for (int i = 0; i < 8 && it.hasPrevious(); ++i) { + int id = it.previous(); + it.remove(); + pool.giveBack(id); + } + } + + // Return all remaining ids + ids.forEach(pool::giveBack); + + // Assert that the recycle pool is empty since all ids were given back + assertEquals(0, pool.getRecyclePoolSize()); + + // Assert that the next id we get is 0 + int id0 = pool.acquire(); + assertEquals(0, id0); + } +} diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/AbstractTestServiceEnhancerResultSetLimits.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/AbstractTestServiceEnhancerResultSetLimits.java index b0ab6669eec..54a7ff59248 100644 --- a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/AbstractTestServiceEnhancerResultSetLimits.java +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/AbstractTestServiceEnhancerResultSetLimits.java @@ -7,30 +7,32 @@ * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * 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. - * - * SPDX-License-Identifier: Apache-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.jena.sparql.service.enhancer.impl; +import static org.junit.jupiter.api.Assertions.assertEquals; + import java.util.IdentityHashMap; import java.util.Map; import java.util.regex.Pattern; +import org.junit.jupiter.api.Test; + import org.apache.jena.query.Dataset; import org.apache.jena.query.DatasetFactory; import org.apache.jena.query.Query; import org.apache.jena.query.QueryExecution; import org.apache.jena.query.QueryFactory; import org.apache.jena.query.ResultSetFactory; +import org.apache.jena.query.ResultSetFormatter; import org.apache.jena.query.ResultSetRewindable; import org.apache.jena.rdf.model.Model; import org.apache.jena.rdf.model.ModelFactory; @@ -43,8 +45,6 @@ import org.apache.jena.sparql.util.Context; import org.apache.jena.vocabulary.RDF; import org.apache.jena.vocabulary.RDFS; -import org.junit.Assert; -import org.junit.Test; public abstract class AbstractTestServiceEnhancerResultSetLimits { @@ -86,7 +86,7 @@ public static Model createModel(int departments) { public void testLoop01_asc_limit1() { Model model = createModel(4); int rows = test(model, "SELECT * { { SELECT ?d { ?d a } ORDER BY ASC(?d) } SERVICE <${mode}> { ?d ?p }}", 1); - Assert.assertEquals(4, rows); + assertEquals(4, rows); } @Test @@ -96,7 +96,7 @@ public void testLoop01_asc_limit2() { Model model = createModel(4); int rows = test(model, "SELECT * { { SELECT ?d { ?d a } ORDER BY ASC(?d) } SERVICE <${mode}> { ?d ?p }}", 2); - Assert.assertEquals(7, rows); + assertEquals(7, rows); } /** Departments in descending order */ @@ -106,7 +106,7 @@ public void testLoop01_desc_limit1() { Model model = createModel(4); int rows = test(model, "SELECT * { { SELECT ?d { ?d a } ORDER BY DESC(?d) } SERVICE <${mode}> { ?d ?p }}", 1); //int rows = test(model, "SELECT * { SELECT ?d { ?d a } ORDER BY DESC(?d) }", 1); - Assert.assertEquals(4, rows); + assertEquals(4, rows); } @Test @@ -116,7 +116,7 @@ public void testLoop01_desc_limit2() { Model model = createModel(4); int rows = test(model, "SELECT * { { SELECT ?d { ?d a } ORDER BY DESC(?d) } SERVICE <${mode}> { ?d ?p }}", 2); - Assert.assertEquals(7, rows); + assertEquals(7, rows); } @@ -129,7 +129,7 @@ public void testLoop01_asc_limit10() { Model model = createModel(4); int rows = test(model, "SELECT * { { SELECT ?d { ?d a } ORDER BY ASC(?d) } SERVICE <${mode}> { ?d ?p }}", 10); - Assert.assertEquals(10, rows); + assertEquals(10, rows); } @@ -155,17 +155,39 @@ public static int testWithCleanCaches(Model model, String queryStr, int hiddenLi public static int testWithCleanCaches(Dataset dataset, String queryStr, int hiddenLimit) { ServiceResultSizeCache.get().invalidateAll(); ServiceResponseCache.get().invalidateAll(); - int result = testCore(dataset, queryStr, hiddenLimit); return result; } - public static int testCore(Model model, String queryStr, int hiddenLimit) { return testCore(identityWrap(model), queryStr, hiddenLimit); } + public static void assertRowCount(int expectedRowCount, Model model, String queryStr, int hiddenLimit) { + assertRowCount(expectedRowCount, identityWrap(model), queryStr, hiddenLimit); + } + + public static void assertRowCount(int expectedRowCount, Dataset dataset, String queryStr, int hiddenLimit) { + ResultSetRewindable rs = exec(dataset, queryStr, hiddenLimit); + int actualSize = rs.size(); + if (actualSize != expectedRowCount) { + rs.reset(); + ResultSetFormatter.outputAsJSON(rs); + } + assertEquals(expectedRowCount, actualSize); + } + public static int testCore(Dataset dataset, String queryStr, int hiddenLimit) { + ResultSetRewindable rs = exec(dataset, queryStr, hiddenLimit); + int result = rs.size(); + return result; + } + + /** + * Execute a query against a dataset and cut off result sets upon reaching "hiddenLimit" result rows. + * The returned ResultSetRewindable is not attached to any resources that might need closing. + */ + public static ResultSetRewindable exec(Dataset dataset, String queryStr, int hiddenLimit) { Query query = QueryFactory.create(queryStr); // Set up a custom service executor registry that "secretly" slices @@ -177,17 +199,16 @@ public static int testCore(Dataset dataset, String queryStr, int hiddenLimit) { return new QueryIterSlice(chain.createExecution(opExec, opOrig, binding, execCxt), 0, hiddenLimit, execCxt); }); - int result; + ResultSetRewindable result; try (QueryExecution qe = QueryExecution.create(query, dataset)) { Context cxt = qe.getContext(); // cxt.put(ARQ.enablePropertyFunctions, true); ServiceEnhancerInit.wrapOptimizer(cxt); ServiceExecutorRegistry.set(qe.getContext(), reg); - ResultSetRewindable rs = ResultSetFactory.makeRewindable(qe.execSelect()); + result = ResultSetFactory.makeRewindable(qe.execSelect()); // ResultSetFormatter.outputAsJSON(rs); - result = rs.size(); } - return result; } + } diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/MoreQueryExecUtils.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/MoreQueryExecUtils.java new file mode 100644 index 00000000000..d378c3fdf3c --- /dev/null +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/MoreQueryExecUtils.java @@ -0,0 +1,77 @@ +/* + * 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.jena.sparql.service.enhancer.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.apache.jena.graph.Node; +import org.apache.jena.query.Query; +import org.apache.jena.sparql.core.Var; +import org.apache.jena.sparql.engine.binding.Binding; +import org.apache.jena.sparql.exec.QueryExec; +import org.apache.jena.sparql.exec.RowSet; +import org.apache.jena.sparql.util.Context; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Iterators; + +public class MoreQueryExecUtils { + public static Node evalToNode(QueryExec qeTmp, Consumer cxtMutator) { + Query query = qeTmp.getQuery(); + Var resultVar = Iterables.getOnlyElement(query.getProjectVars()); + Binding binding = evalToBinding(qeTmp, cxtMutator); + Node result = binding == null ? null : binding.get(resultVar); + return result; + } + + public static Binding evalToBinding(QueryExec qeTmp, Consumer cxtMutator) { + Binding result; + try (QueryExec qe = qeTmp) { + if (cxtMutator != null) { + cxtMutator.accept(qe.getContext()); + } + RowSet rs = qe.select(); + result = Iterators.getOnlyElement(rs, null); + } + return result; + } + + // Project a certain column into a list of nodes + public static List evalToNodes(QueryExec qeTmp, Consumer cxtMutator) { + Query query = qeTmp.getQuery(); + Var resultVar = Iterables.getOnlyElement(query.getProjectVars()); + List result = new ArrayList<>(); + try (QueryExec qe = qeTmp) { + if (cxtMutator != null) { + cxtMutator.accept(qe.getContext()); + } + qe.select().forEachRemaining(b -> result.add(b.get(resultVar))); + } + return result; + } + + public static String evalToLexicalForm(QueryExec qe, Consumer cxtMutator) { + Node node = evalToNode(qe, cxtMutator); + String result = node == null ? null : + node.isLiteral() ? node.getLiteralLexicalForm() : node.toString(); + return result; + } +} diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestLinkedList.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestLinkedList.java new file mode 100644 index 00000000000..30d38e5e319 --- /dev/null +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestLinkedList.java @@ -0,0 +1,138 @@ +/* + * 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.jena.sparql.service.enhancer.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.function.Predicate; + +import com.google.common.collect.Range; +import com.google.common.collect.RangeSet; +import com.google.common.collect.TreeRangeSet; + +import org.junit.jupiter.api.Test; + +import org.apache.jena.sparql.service.enhancer.claimingcache.PredicateRangeSet; +import org.apache.jena.sparql.service.enhancer.claimingcache.PredicateTrue; +import org.apache.jena.sparql.service.enhancer.util.LinkedList; + +public class TestLinkedList { + @Test + public void testEmptyList() { + List list = new LinkedList<>(List.of()); + ListIterator it = list.listIterator(0); + assertFalse(it.hasNext()); + assertFalse(it.hasPrevious()); + } + + @Test + public void testSingletonList() { + List list = new LinkedList<>(List.of(0)); + ListIterator it = list.listIterator(0); + int v; + + // Test alternation of next/prev. + for (int i = 0; i < 1; ++i) { + assertTrue(it.hasNext()); + assertFalse(it.hasPrevious()); + v = it.next(); + assertEquals(0, v); + + assertFalse(it.hasNext()); + assertTrue(it.hasPrevious()); + v = it.previous(); + assertEquals(0, v); + } + } + + @Test + public void reverseTraversal() { + List list = new LinkedList<>(List.of(0, 1, 2)); + ListIterator it = list.listIterator(3); + for (int expected = 2; expected >= 0; --expected) { + int actual = it.previous(); + assertEquals(expected, actual); + } + } + + @Test + public void positionNearEnd() { + List list = new LinkedList<>(List.of(0, 1, 2, 3, 4)); + ListIterator it = list.listIterator(3); + + List actualTail = new ArrayList<>(); + it.forEachRemaining(actualTail::add); + + List expectedTail = List.of(3, 4); + assertEquals(expectedTail, actualTail); + } + + @Test + public void positionNearStart() { + List list = new LinkedList<>(List.of(0, 1, 2, 3, 4)); + ListIterator it = list.listIterator(2); + + List actualHead = new ArrayList<>(); + while (it.hasPrevious()) { + actualHead.add(it.previous()); + } + + List expectedHead = List.of(1, 0); + assertEquals(expectedHead, actualHead); + } + + @Test + public void testInsert() { + List actual = new LinkedList<>(List.of(0, 1, 2)); + ListIterator it = actual.listIterator(1); + it.add(3); + it.add(4); + + List expected = List.of(0, 4, 3, 1, 2); + assertEquals(expected, actual); + } + + @Test + public void testRanges() { + RangeSet rs = TreeRangeSet.create(); + rs.add(Range.closedOpen(0l, 11l)); + + LinkedList> actual = new LinkedList<>(); + actual.append(PredicateTrue.get()); + actual.append(PredicateTrue.get()); + actual.append(new PredicateRangeSet<>(rs)); + + List> expected = List.of(PredicateTrue.get(), new PredicateRangeSet<>(rs)); + + Iterator it = actual.iterator(); + it.hasNext(); + it.next(); + + it.next(); + it.remove(); + + assertEquals(expected, actual); + } +} diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestRequestExecutorBase.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestRequestExecutorBase.java new file mode 100644 index 00000000000..fbbcecd1f09 --- /dev/null +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestRequestExecutorBase.java @@ -0,0 +1,108 @@ +/* + * 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.jena.sparql.service.enhancer.impl; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.IntStream; + +import com.google.common.base.Stopwatch; +import com.google.common.collect.Iterators; + +import org.junit.jupiter.api.Test; + +import org.apache.jena.atlas.io.IndentedWriter; +import org.apache.jena.sparql.serializer.SerializationContext; +import org.apache.jena.sparql.service.enhancer.impl.RequestExecutorBase.Granularity; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterator; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterators; + +public class TestRequestExecutorBase { + @Test + public void testWrapper() { + for (int i = 0; i < 2; ++i) { + test(); + } + } + + public void test() { + int numGroups = 1000; + int numItemsPerGroup = 10; + + int taskSlots = 20; + int readAhead = 100000; + + Iterator> plainIt = IntStream.range(0, numGroups).boxed() + .flatMap(i -> IntStream.range(0, numItemsPerGroup).mapToObj(j -> Map.entry("group" + i, "item" + j))) + .iterator(); + + Batcher> batcher = new Batcher<>(Entry::getKey, 3, 10); + + AbortableIterator>> batchedIt = batcher.batch(AbortableIterators.wrap(plainIt)); + + System.out.println(batchedIt.toString()); + // if (true) { return; } + + RequestExecutorBase, Entry> executor = new RequestExecutorBase<>( + new AtomicBoolean(), + Granularity.BATCH, + batchedIt, + taskSlots, + readAhead) { + // The creator may be called from the main thread - but what it returns will be run on a separate thread. + @Override + protected IteratorCreator> processBatch(boolean isInNewThread, String groupKey, + List> batch, List reverseMap) { + + // Create an iterator that defers each item by 1 ms. + return () -> AbortableIterators.wrap(IntStream.range(0, batch.size()) + .peek(item -> { + try { + Thread.sleep(1); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }) + .mapToObj(i -> Map.entry(reverseMap.get(i), batch.get(i).getValue())) + .iterator()); + } + + @Override protected long extractInputOrdinal(Entry input) { return input.getKey(); } + @Override protected void checkCanExecInNewThread() {} + @Override public void output(IndentedWriter out, SerializationContext sCxt) {} + @Override protected boolean isCancelled() { return false; } + }; + + + Stopwatch sw = Stopwatch.createStarted(); + int size = Iterators.size(executor); + System.out.println(size); + System.out.println("Time taken: " + sw.elapsed(TimeUnit.MILLISECONDS) * 0.001f); +// while (executor.hasNext()) { +// Entry item = executor.next(); +// System.out.println(item); +// } + + executor.close(); + } +} diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerBatchQueryRewriter.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerBatchQueryRewriter.java index a70e8c33e18..c1899e00a9e 100644 --- a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerBatchQueryRewriter.java +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerBatchQueryRewriter.java @@ -7,25 +7,26 @@ * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * 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. - * - * SPDX-License-Identifier: Apache-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.jena.sparql.service.enhancer.impl; +import static org.junit.jupiter.api.Assertions.assertEquals; + import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; import org.apache.jena.query.Query; @@ -39,9 +40,8 @@ import org.apache.jena.sparql.core.Var; import org.apache.jena.sparql.engine.binding.Binding; import org.apache.jena.sparql.engine.binding.BindingFactory; +import org.apache.jena.sparql.service.enhancer.impl.BatchQueryRewriter.SubstitutionStrategy; import org.apache.jena.sparql.syntax.syntaxtransform.QueryTransformOps; -import org.junit.Assert; -import org.junit.Test; public class TestServiceEnhancerBatchQueryRewriter { @@ -91,7 +91,7 @@ public void testOrderBy_01() { " }", "ORDER BY ASC(?idx) ?s")); - Assert.assertEquals(expectedQuery, actualQuery); + assertEquals(expectedQuery, actualQuery); } /** GH-2992: Blank nodes must be relabeled wher building batch unions. */ @@ -129,11 +129,17 @@ public void testReport_01() { ORDER BY ASC(?idx) """)); - Assert.assertEquals(expectedQuery, actualQuery); + assertEquals(expectedQuery, actualQuery); } private static Query defaultRewrite(OpService op , Batch> batch) { - BatchQueryRewriter rewriter = new BatchQueryRewriter(new OpServiceInfo(op), Var.alloc("idx"), false, false, false); + BatchQueryRewriter rewriter = BatchQueryRewriterBuilder.from(new OpServiceInfo(op), Var.alloc("idx")) + .setSequentialUnion(false) + .setOrderRetainingUnion(false) + .setOmitEndMarker(false) + .setSubstitutionStrategy(SubstitutionStrategy.SUBSTITUTE) + .build(); + BatchQueryRewriteResult rewrite = rewriter.rewrite(batch); Op resultOp = rewrite.getOp(); Query result = harmonizeBnodes(OpAsQuery.asQuery(resultOp)); diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerBatcher.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerBatcher.java index ef450e9c8c7..e34ec9ff7f1 100644 --- a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerBatcher.java +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerBatcher.java @@ -7,20 +7,20 @@ * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * 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. - * - * SPDX-License-Identifier: Apache-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.jena.sparql.service.enhancer.impl; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -28,11 +28,12 @@ import java.util.Random; import java.util.stream.Stream; -import org.apache.jena.atlas.iterator.Iter; -import org.apache.jena.atlas.iterator.IteratorCloseable; import com.google.common.collect.Streams; -import org.junit.Assert; -import org.junit.Test; + +import org.junit.jupiter.api.Test; + +import org.apache.jena.atlas.iterator.IteratorCloseable; +import org.apache.jena.sparql.service.enhancer.impl.util.iterator.AbortableIterators; public class TestServiceEnhancerBatcher { @@ -97,7 +98,7 @@ public static void eval( int maxOutOfBandItemCount, List> expectedBatchIds) { IteratorCloseable>> it = new Batcher> - (Entry::getKey, maxBatchSize, maxOutOfBandItemCount).batch(Iter.iter(input.iterator())); + (Entry::getKey, maxBatchSize, maxOutOfBandItemCount).batch(AbortableIterators.wrap(input.iterator())); // it.forEachRemaining(System.err::println); // For each obtained batch extract the list of values @@ -105,7 +106,7 @@ public static void eval( .map(groupedBatch -> groupedBatch.getBatch().getItems().values().stream().map(Entry::getValue).toList()) .toList(); - Assert.assertEquals(expectedBatchIds, actualBatchIds); + assertEquals(expectedBatchIds, actualBatchIds); } /** This test creates random input, batches it and checks that the number of batched items matches that of the input */ @@ -129,15 +130,15 @@ public void testBatcher_largeInput() { } Stream>> stream = Streams.stream( - new Batcher>(Entry::getKey, 4, 4).batch(Iter.iter(testData.iterator()))); + new Batcher>(Entry::getKey, 4, 4).batch(AbortableIterators.wrap(testData.iterator()))); int actualItemCount = stream.mapToInt(groupedBatch -> { int r = groupedBatch.getBatch().getItems().size(); // Sanity check that batches are not larger than the allowed maximum size - Assert.assertTrue("Batch exceeded maximum size", r <= maxBatchSize); + assertTrue(r <= maxBatchSize, "Batch exceeded maximum size"); return r; }).sum(); - Assert.assertEquals(actualItemCount, expectedItemCount); + assertEquals(actualItemCount, expectedItemCount); } } diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerCachedVsUncached.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerCachedVsUncached.java index a2444fe7f6c..3b11305f6f1 100644 --- a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerCachedVsUncached.java +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerCachedVsUncached.java @@ -7,20 +7,19 @@ * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * 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. - * - * SPDX-License-Identifier: Apache-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.jena.sparql.service.enhancer.impl; +import static org.junit.jupiter.api.Assertions.assertTrue; + import java.util.ArrayList; import java.util.Collection; import java.util.List; @@ -28,14 +27,16 @@ import java.util.function.Consumer; import java.util.regex.Pattern; -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; import org.apache.jena.atlas.logging.Log; -import org.apache.jena.query.*; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryExecution; +import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.ResultSetFactory; +import org.apache.jena.query.ResultSetFormatter; +import org.apache.jena.query.ResultSetRewindable; import org.apache.jena.rdf.model.Model; import org.apache.jena.sparql.engine.iterator.QueryIterSlice; import org.apache.jena.sparql.resultset.ResultsCompare; @@ -50,70 +51,12 @@ * Furthermore, requests with / without use of 'bulk:' and 'loop:' (with an empty input binding) can also be compared. * Query results should be the same regardless of whether these options are present or absent. */ -@RunWith(Parameterized.class) public class TestServiceEnhancerCachedVsUncached { - protected String name; - protected String queryStrA; - protected String queryStrB; - protected Model model; - protected Consumer cxtMutator; - - public TestServiceEnhancerCachedVsUncached(String name, String queryStrA, String queryStrB, Model model, Consumer cxtMutator) { - super(); - this.name = name; - this.queryStrA = queryStrA; - this.queryStrB = queryStrB; - this.model = model; - this.cxtMutator = cxtMutator; - } - - @Test - public void test() { - Log.debug(TestServiceEnhancerCachedVsUncached.class, "Query A: " + queryStrA); - Log.debug(TestServiceEnhancerCachedVsUncached.class, "Query B: " + queryStrB); - - // Debug flag: If onlyA is true then no comparison with queryB is made - boolean onlyA = false; - - Query queryA = QueryFactory.create(queryStrA); - ResultSetRewindable rsA; - try (QueryExecution qeA = QueryExecution.create(queryA, model)) { - cxtMutator.accept(qeA.getContext()); - rsA = ResultSetFactory.makeRewindable(qeA.execSelect()); - - if (!onlyA) { - Query queryB = QueryFactory.create(queryStrB); - ResultSetRewindable rsB; - try (QueryExecution qeB = QueryExecution.create(queryB, model)) { - cxtMutator.accept(qeB.getContext()); - rsB = ResultSetFactory.makeRewindable(qeB.execSelect()); - - boolean isEqual = ResultsCompare.equalsByValue(rsA, rsB); - if (!isEqual) { - rsA.reset(); - ResultSetFormatter.out(System.out, rsA); - - rsB.reset(); - ResultSetFormatter.out(System.out, rsB); - } - Assert.assertTrue(isEqual); - } - } else { - rsA.reset(); - System.out.println("Got " + ResultSetFormatter.consume(rsA) + " results"); - } - - } - } - - - - - @Parameters(name = "SPARQL Cache Test {index}: {0}") - public static Collection data() + // @Parameters(name = "SPARQL Cache Test {index}: {0}") + @TestFactory + public Collection generateTests() throws Exception - { int randomSeed = 42; Random random = new Random(randomSeed); @@ -136,7 +79,7 @@ public static Collection data() ); - List pool = new ArrayList<>(); + List dynamicTests = new ArrayList<>(); for (int i = 0; i < 1000; ++i) { int bulkSizeA = random.nextInt(9) + 1; @@ -154,7 +97,6 @@ public static Collection data() String strA = strBase .replaceAll(Pattern.quote("${mode}"), "cache:loop:bulk+" + bulkSizeA + ":"); - String strB = strBase .replaceAll(Pattern.quote("${mode}"), "loop:bulk+" + bulkSizeB + ":"); @@ -165,10 +107,66 @@ public static Collection data() ServiceExecutorRegistry.set(cxt, reg); }; - pool.add(new Object[] { "test" + i, strA, strB, model, cxtMutator }); + String testName = String.format("SPARQL Cache Test %d: %s", i, info); + DynamicTest dynamicTest = DynamicTest.dynamicTest(testName, + () -> new TestCase(testName, strA, strB, model, cxtMutator).test()); + dynamicTests.add(dynamicTest); } - return pool; + return dynamicTests; } + private static class TestCase { + protected String name; + protected String queryStrA; + protected String queryStrB; + protected Model model; + protected Consumer cxtMutator; + + public TestCase(String name, String queryStrA, String queryStrB, Model model, Consumer cxtMutator) { + super(); + this.name = name; + this.queryStrA = queryStrA; + this.queryStrB = queryStrB; + this.model = model; + this.cxtMutator = cxtMutator; + } + + public void test() { + Log.debug(TestServiceEnhancerCachedVsUncached.class, "Query A: " + queryStrA); + Log.debug(TestServiceEnhancerCachedVsUncached.class, "Query B: " + queryStrB); + + // Debug flag: If onlyA is true then no comparison with queryB is made + boolean onlyA = false; + + Query queryA = QueryFactory.create(queryStrA); + ResultSetRewindable rsA; + try (QueryExecution qeA = QueryExecution.create(queryA, model)) { + cxtMutator.accept(qeA.getContext()); + rsA = ResultSetFactory.makeRewindable(qeA.execSelect()); + + if (!onlyA) { + Query queryB = QueryFactory.create(queryStrB); + ResultSetRewindable rsB; + try (QueryExecution qeB = QueryExecution.create(queryB, model)) { + cxtMutator.accept(qeB.getContext()); + rsB = ResultSetFactory.makeRewindable(qeB.execSelect()); + + boolean isEqual = ResultsCompare.equalsByValue(rsA, rsB); + if (!isEqual) { + rsA.reset(); + ResultSetFormatter.out(System.out, rsA); + + rsB.reset(); + ResultSetFormatter.out(System.out, rsB); + } + assertTrue(isEqual); + } + } else { + rsA.reset(); + System.out.println("Got " + ResultSetFormatter.consume(rsA) + " results"); + } + } + } + } } diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerMisc.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerMisc.java index f95d302b74f..afea3f9d162 100644 --- a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerMisc.java +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerMisc.java @@ -7,21 +7,23 @@ * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * 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. - * - * SPDX-License-Identifier: Apache-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.jena.sparql.service.enhancer.impl; +import static org.junit.jupiter.api.Assertions.assertEquals; + import com.google.common.base.StandardSystemProperty; + +import org.junit.jupiter.api.Test; + import org.apache.jena.graph.NodeFactory; import org.apache.jena.query.ARQ; import org.apache.jena.query.Dataset; @@ -40,19 +42,26 @@ import org.apache.jena.sparql.algebra.Algebra; import org.apache.jena.sparql.algebra.Op; import org.apache.jena.sparql.algebra.OpAsQuery; +import org.apache.jena.sparql.algebra.Table; +import org.apache.jena.sparql.algebra.TableFactory; import org.apache.jena.sparql.algebra.Transform; import org.apache.jena.sparql.algebra.Transformer; import org.apache.jena.sparql.algebra.optimize.Optimize; import org.apache.jena.sparql.core.Substitute; import org.apache.jena.sparql.core.Var; import org.apache.jena.sparql.engine.binding.BindingFactory; +import org.apache.jena.sparql.exec.QueryExec; +import org.apache.jena.sparql.exec.QueryExecDataset; +import org.apache.jena.sparql.exec.RowSet; import org.apache.jena.sparql.service.enhancer.algebra.TransformSE_JoinStrategy; import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerConstants; -import org.junit.Assert; -import org.junit.Test; +import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerInit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** Miscellaneous tests for many aspects of the service enhancer plugin. */ public class TestServiceEnhancerMisc { + private static final Logger logger = LoggerFactory.getLogger(TestServiceEnhancerMisc.class); @Test public void testLargeCache01() { @@ -65,7 +74,7 @@ public void testLargeCache01() { int evalRowCount = AbstractTestServiceEnhancerResultSetLimits.testWithCleanCaches(model, queryStr, 1000000000); int cachedRowCount = AbstractTestServiceEnhancerResultSetLimits.testCore(model, queryStr, 1000000000); - Assert.assertEquals(evalRowCount, cachedRowCount); + assertEquals(evalRowCount, cachedRowCount); } /** A query where it's whole graph pattern is subject to caching */ @@ -77,14 +86,14 @@ public void testCacheFullQuery() { Model model = AbstractTestServiceEnhancerResultSetLimits.createModel(1000); int rows = AbstractTestServiceEnhancerResultSetLimits.testCore(model, "SELECT * { SERVICE { SELECT DISTINCT ?p { ?s ?p ?o } } }", 100); - Assert.assertEquals(3, rows); + assertEquals(3, rows); // TODO We need to ensure that no backend request is made // We could register a custom service executor that does the counting // And/Or the test runner could return a stats object which includes the number of backend requests int cachedRows = AbstractTestServiceEnhancerResultSetLimits.testCore(model, "SELECT * { SERVICE { SELECT DISTINCT ?p { ?s ?p ?o } } }", 100); - Assert.assertEquals(3, cachedRows); + assertEquals(3, cachedRows); } @Test @@ -99,7 +108,7 @@ public void testNestedLoopWithPropertyFunction() { Model model = AbstractTestServiceEnhancerResultSetLimits.createModel(10); int rows = AbstractTestServiceEnhancerResultSetLimits.testWithCleanCaches(model, queryStr, 1000); - Assert.assertEquals(110, rows); + assertEquals(110, rows); } /** Tests that a loop join where the scoped visible variables on either side are disjoint @@ -114,7 +123,7 @@ public void testLoopJoinWithScope() { Model model = AbstractTestServiceEnhancerResultSetLimits.createModel(9); int rows = AbstractTestServiceEnhancerResultSetLimits.testWithCleanCaches(model, queryStr, 1000); - Assert.assertEquals(9, rows); + assertEquals(9, rows); } @Test @@ -127,9 +136,9 @@ public void testLookupJoinWithScopeAndCache() { Model model = AbstractTestServiceEnhancerResultSetLimits.createModel(9); int referenceRowCount = AbstractTestServiceEnhancerResultSetLimits.testWithCleanCaches(model, queryStr, 1000); - Assert.assertEquals(9, referenceRowCount); + assertEquals(9, referenceRowCount); int actualRowCount = AbstractTestServiceEnhancerResultSetLimits.testCore(model, queryStr, 1000); - Assert.assertEquals(referenceRowCount, actualRowCount); + assertEquals(referenceRowCount, actualRowCount); /* ------------------------------- @@ -160,7 +169,7 @@ public void testStdJoinWithScope() { Model model = AbstractTestServiceEnhancerResultSetLimits.createModel(9); int actualRowCount = AbstractTestServiceEnhancerResultSetLimits.testWithCleanCaches(model, queryStr, 1000); - Assert.assertEquals(9, actualRowCount); + assertEquals(9, actualRowCount); } @Test @@ -177,12 +186,27 @@ public void testNestedCache() { int referenceRowCount = AbstractTestServiceEnhancerResultSetLimits.testWithCleanCaches(model, queryStr, 1000); int actualRowCount = AbstractTestServiceEnhancerResultSetLimits.testCore(model, queryStr, 1000); - Assert.assertEquals(3, referenceRowCount); - Assert.assertEquals(referenceRowCount, actualRowCount); + assertEquals(3, referenceRowCount); + assertEquals(referenceRowCount, actualRowCount); + } + + @Test + public void testCacheRefresh_01a() { + String queryStr = String.join("\n", + "SELECT * {", + " SERVICE { SELECT ?s { ?s a } ORDER BY ?s OFFSET 7 LIMIT 2 }", + "}"); + + Model model = AbstractTestServiceEnhancerResultSetLimits.createModel(9); + int referenceRowCount = AbstractTestServiceEnhancerResultSetLimits.testWithCleanCaches(model, queryStr, 1000); + int actualRowCount = AbstractTestServiceEnhancerResultSetLimits.testCore(model, queryStr, 1000); + + assertEquals(2, referenceRowCount); + assertEquals(referenceRowCount, actualRowCount); } @Test - public void testCacheRefresh() { + public void testCacheRefresh_01b() { String queryStr = String.join("\n", "SELECT * {", " SERVICE {", @@ -195,8 +219,8 @@ public void testCacheRefresh() { int referenceRowCount = AbstractTestServiceEnhancerResultSetLimits.testWithCleanCaches(model, queryStr, 1000); int actualRowCount = AbstractTestServiceEnhancerResultSetLimits.testCore(model, queryStr, 1000); - Assert.assertEquals(3, referenceRowCount); - Assert.assertEquals(referenceRowCount, actualRowCount); + assertEquals(3, referenceRowCount); + assertEquals(referenceRowCount, actualRowCount); } @Test @@ -214,10 +238,11 @@ public void testCacheRefreshWithOffsetOutside() { queryStr = queryStr.replace("cache:", "cache+clear:"); int actualRowCount = AbstractTestServiceEnhancerResultSetLimits.testCore(model, queryStr, 1000); - Assert.assertEquals(4, referenceRowCount); - Assert.assertEquals(referenceRowCount, actualRowCount); + assertEquals(4, referenceRowCount); + assertEquals(referenceRowCount, actualRowCount); } + /** Test case where LIMIT/OFFSET is used within the SERVICE block. */ @Test public void testCacheRefreshWithOffsetInside() { String queryStr = String.join("\n", @@ -234,9 +259,9 @@ public void testCacheRefreshWithOffsetInside() { queryStr = queryStr.replace("cache:", "cache+clear:"); int rows3 = AbstractTestServiceEnhancerResultSetLimits.testCore(model, queryStr, 1000); - Assert.assertEquals(4, rows); - Assert.assertEquals(rows, rows2); - Assert.assertEquals(rows, rows3); + assertEquals(4, rows); + assertEquals(rows, rows2); + assertEquals(rows, rows3); } @Test @@ -263,7 +288,7 @@ public void testSubstitute() { Op op = Algebra.compile(QueryFactory.create(queryStr)); Op op2 = Substitute.substitute(op, BindingFactory.binding(Var.alloc("s"), NodeFactory.createURI("urn:s"))); Query actualQuery = OpAsQuery.asQuery(op2); - Assert.assertEquals(expectedQuery, actualQuery); + assertEquals(expectedQuery, actualQuery); } /** Tests for the presence of the function cacheInvalidate and expects it to return one binding @@ -278,24 +303,22 @@ public void testCacheMgmtInvalidate() { Dataset dataset = DatasetFactory.create(); dataset.getContext().set(ServiceEnhancerConstants.enableMgmt, true); int actualRowCount = AbstractTestServiceEnhancerResultSetLimits.testWithCleanCaches(dataset, queryStr, 1000); - Assert.assertEquals(1, actualRowCount); + assertEquals(1, actualRowCount); } /** Tests whether cacheLs with empty argument only lists the ids */ @Test public void testCacheMgmtList01() { - // This call creates one cache entry testCacheRefreshWithOffsetInside(); String queryStr = String.join("\n", "PREFIX se: ", "SELECT * WHERE {", - " ?id se:cacheLs ()", + " ?id se:cacheLs (?service ?queryStr)", "}"); - int actualRowCount = AbstractTestServiceEnhancerResultSetLimits.testCore(ModelFactory.createDefaultModel(), queryStr, 1000); - Assert.assertEquals(1, actualRowCount); + AbstractTestServiceEnhancerResultSetLimits.assertRowCount(1, ModelFactory.createDefaultModel(), queryStr, 1000); } /** Tests for the presence of the property function cacheLs */ @@ -312,7 +335,7 @@ public void testCacheMgmtList02() { "}"); int actualRowCount = AbstractTestServiceEnhancerResultSetLimits.testCore(ModelFactory.createDefaultModel(), queryStr, 1000); - Assert.assertEquals(1, actualRowCount); + assertEquals(1, actualRowCount); } @Test @@ -339,7 +362,7 @@ public void testWikiData() { int referenceRowCount = AbstractTestServiceEnhancerResultSetLimits.testWithCleanCaches(dataset, queryStr, 1000); int actualRowCount = AbstractTestServiceEnhancerResultSetLimits.testCore(dataset, queryStr, 1000); - Assert.assertEquals(referenceRowCount, actualRowCount); + assertEquals(referenceRowCount, actualRowCount); } @Test @@ -356,7 +379,7 @@ public void testSubSelectInService() { Model model = AbstractTestServiceEnhancerResultSetLimits.createModel(9); int actualRowCount = AbstractTestServiceEnhancerResultSetLimits.testWithCleanCaches(model, queryStr, 1000); - Assert.assertEquals(6, actualRowCount); + assertEquals(6, actualRowCount); } @Test @@ -387,7 +410,7 @@ public void testLoopScope() { Op op2 = Optimize.stdOptimizationFactory.create(ARQ.getContext()).rewrite(op); Op op3 = Transformer.transform(new TransformSE_JoinStrategy(), op2); - Assert.assertEquals(expectedStr, op3.toString()); + assertEquals(expectedStr, op3.toString()); } @Test @@ -410,9 +433,9 @@ public void testScope3() { "}"); //.replace("loop:", "urn:x-arq:self"); int referenceRowCount = AbstractTestServiceEnhancerResultSetLimits.testWithCleanCaches(dataset, queryStr, 1000); - Assert.assertEquals(3, referenceRowCount); + assertEquals(3, referenceRowCount); int actualRowCount = AbstractTestServiceEnhancerResultSetLimits.testCore(dataset, queryStr, 1000); - Assert.assertEquals(referenceRowCount, actualRowCount); + assertEquals(referenceRowCount, actualRowCount); } @Test @@ -448,7 +471,7 @@ public void testScopeSimple() { Op op2 = Optimize.stdOptimizationFactory.create(ARQ.getContext()).rewrite(op1); Op op3 = Transformer.transform(loopTransform, op2); - Assert.assertEquals(expectedStr, op3.toString()); + assertEquals(expectedStr, op3.toString()); } @Test @@ -470,7 +493,7 @@ public void testNormalization01() { + "}"; int actualRowCount = AbstractTestServiceEnhancerResultSetLimits.testWithCleanCaches(dataset, queryStr, 1000); - Assert.assertEquals(4, actualRowCount); + assertEquals(4, actualRowCount); } /** @@ -519,7 +542,119 @@ public void testBulkRequestsOverCachedEmptyResultSets() { rsSize = ResultSetFormatter.consume(rs); } // Expect the 3 labels of the test dataset - Assert.assertEquals(3, rsSize); + assertEquals(3, rsSize); + } + } + + /** Test case where an attempt is made to cache slightly more items than the maximum cache size. */ + @Test + public void testCacheEvictionCornerCase() { + // Investigation of some some very rare race conditions due to cache thrashing + // required around 100K+ tests. + int numTests = 100; + int maxCacheSize = 10; + int numExcessItems = 1; // Number of items by which to exceed the maximum cache size. + testCacheEvictionCornerCaseWorker(numTests, maxCacheSize, numExcessItems); + // IntStream.range(0, 20).boxed().toList().parallelStream().forEach(i -> testCacheEvictionCornerCaseWorker()); + } + + public void testCacheEvictionCornerCaseWorker(int numTests, int maxCacheSize, int numExcessItems) { + Model model = AbstractTestServiceEnhancerResultSetLimits.createModel(maxCacheSize + numExcessItems); + Dataset ds = DatasetFactory.wrap(model); + ServiceResponseCache cache = new ServiceResponseCache(1, 1, maxCacheSize); + ServiceResponseCache.set(ds.getContext(), cache); + + String queryStr = """ + SELECT * { + { SELECT ?dept { ?dept a } ORDER BY ?dept LIMIT $N } + SERVICE { SELECT ?dept (COUNT(*) AS ?employees) { ?dept ?emp } GROUP BY ?dept } + } + """.replace("$N", "" + (maxCacheSize + numExcessItems)); + Query query = QueryFactory.create(queryStr); + + Table prevTable = null; + for (int i = 0; i < numTests; ++i) { + Table thisTable; + try (QueryExec qe = QueryExecDataset.newBuilder() + .dataset(ds.asDatasetGraph()) + .query(query) + .build()) { + thisTable = TableFactory.create(qe.select()); + } + + if (prevTable != null) { + if (!prevTable.equals(thisTable)) { + System.err.println("Test failure on iteration #" + i); + } + assertEquals(prevTable, thisTable); + } else { + prevTable = thisTable; + } + + if (i % 10 == 0) { + cache.invalidateAll(); + } + } + } + + @Test + public void testCacheEvictionCornerCase2() { + // Investigation of some some very rare race conditions due to cache thrashing. + // There was a bug in QueryIterBulkAndCache.moveToNext when backend requests were + // followed by cached ranges: In that case iterator ended too early not serving the cached data. + // Reproduction required around 100K+ iterations. + int numTests = 100; + int maxCacheSize = 10; + int numExcessItems = 1; // Number of items by which to exceed the maximum cache size. + testCacheEvictionCornerCaseWorker2(numTests, maxCacheSize, numExcessItems); + // IntStream.range(0, 20).boxed().toList().parallelStream().forEach(i -> testCacheEvictionCornerCaseWorker()); + } + + public void testCacheEvictionCornerCaseWorker2(int numTests, int maxCacheSize, int numExcessItems) { + Dataset ds = RDFDataMgr.loadDataset("linkedgeodata.sample.ttl"); + ServiceResponseCache cache = new ServiceResponseCache(1, 1, maxCacheSize); + ServiceResponseCache.set(ds.getContext(), cache); + + String queryStr = """ + PREFIX owl: + SELECT * { + { SELECT ?t { ?t a owl:Class } LIMIT $N } + SERVICE { SELECT ?s ?t { ?s a ?t } LIMIT 2 } + } + """.replace("$N", "" + (maxCacheSize + numExcessItems)); + Query query = QueryFactory.create(queryStr); + + Table prevTable = null; + for (int i = 0; i < numTests; ++i) { + Table thisTable; + try (QueryExec qe = QueryExecDataset.newBuilder() + .dataset(ds.asDatasetGraph()) + .query(query) + .build()) { + ServiceEnhancerInit.wrapOptimizer(qe.getContext()); + + RowSet rs = qe.select(); + thisTable = TableFactory.create(rs); + } + + if (logger.isDebugEnabled()) { + logger.debug(ResultSetFormatter.asText(ResultSet.adapt(thisTable.toRowSet()))); + } + + if (prevTable != null) { + if (!prevTable.equals(thisTable)) { + if (logger.isErrorEnabled()) { + logger.error("Test failure on iteration #" + i); + } + } + assertEquals(prevTable, thisTable); + } else { + prevTable = thisTable; + } + +// if (i % 10 == 0) { +// cache.invalidateAll(); +// } } } } diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerQueryExecutionCancel.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerQueryExecutionCancel.java new file mode 100644 index 00000000000..3fb940673cb --- /dev/null +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerQueryExecutionCancel.java @@ -0,0 +1,130 @@ +/* + * 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.jena.sparql.service.enhancer.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.junit.jupiter.api.Test; + +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryCancelledException; +import org.apache.jena.query.QueryExecution; +import org.apache.jena.query.QueryExecutionFactory; +import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.ResultSet; +import org.apache.jena.query.ResultSetFormatter; +import org.apache.jena.rdf.model.Model; + + +public class TestServiceEnhancerQueryExecutionCancel { + + /** Test cancellation of caching a large result set. */ + @Test + public void test_01() { + // LogCtl.setLogging(); + + int maxCancelDelayInMillis = 100; + + int cpuCount = Runtime.getRuntime().availableProcessors(); + // Spend at most roughly 1 second per cpu (10 tasks a max 100ms) + int taskCount = cpuCount * 10; + + Model model = AbstractTestServiceEnhancerResultSetLimits.createModel(1000); + + // Produce a sufficiently large result set so that abort will surely hit in mid-execution + // Query query = QueryFactory.create("SELECT * { SERVICE { ?a ?b ?c . ?d ?e ?f . ?g ?h ?i . ?j ?k ?l } }"); + Query query = QueryFactory.create("SELECT * { SERVICE { ?a ?b ?c . ?d ?e ?f . ?g ?h ?i . ?j ?k ?l } }"); + + // The query without the cache block: + // Query query = QueryFactory.create("SELECT * { ?a ?b ?c . ?d ?e ?f . ?g ?h ?i . ?j ?k ?l }"); + + Callable qeFactory = () -> QueryExecutionFactory.create(query, model); + + runConcurrentAbort(taskCount, maxCancelDelayInMillis, qeFactory, TestServiceEnhancerQueryExecutionCancel::doCount); + } + + /** + * Copy of TestQueryExecutionCancel.runConcurrentAbort. + */ + public static void runConcurrentAbort(int taskCount, int maxCancelDelay, Callable qeFactory, Function processor) { + Random cancelDelayRandom = new Random(); + ExecutorService executorService = Executors.newCachedThreadPool(); + try { + List list = IntStream.range(0, taskCount).boxed().collect(Collectors.toList()); + list + .parallelStream() + .forEach(i -> { + QueryExecution qe; + try { + qe = qeFactory.call(); + } catch (Exception e) { + throw new RuntimeException("Failed to build a query execution", e); + } + Future future = executorService.submit(() -> processor.apply(qe)); + int delayToAbort = cancelDelayRandom.nextInt(maxCancelDelay); + try { + Thread.sleep(delayToAbort); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + // System.out.println("Abort: " + qe); + qe.abort(); + try { + // System.out.println("Waiting for: " + qe); + future.get(); + } catch (ExecutionException e) { + Throwable cause = e.getCause(); + if (!(cause instanceof QueryCancelledException)) { + // Unexpected exception - print out the stack trace + e.printStackTrace(); + } + assertEquals(QueryCancelledException.class, cause.getClass()); + } catch (InterruptedException e) { + // Ignored + } finally { + // System.out.println("Completed: " + qe); + } + }); + } finally { + executorService.shutdownNow(); + } + } + + /** + * Copy of TestQueryExecutionCancel.doCount. + */ + private static final int doCount(QueryExecution qe) { + try (QueryExecution qe2 = qe) { + ResultSet rs = qe2.execSelect(); + int size = ResultSetFormatter.consume(rs); + return size; + } + } +} diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerResultSetLimitsWithCache.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerResultSetLimitsWithCache.java index 355869b620f..1e4e436095e 100644 --- a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerResultSetLimitsWithCache.java +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerResultSetLimitsWithCache.java @@ -7,16 +7,13 @@ * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * 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. - * - * SPDX-License-Identifier: Apache-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.jena.sparql.service.enhancer.impl; diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerResultSetLimitsWithoutCache.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerResultSetLimitsWithoutCache.java index 0e5038e462c..908216742bd 100644 --- a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerResultSetLimitsWithoutCache.java +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerResultSetLimitsWithoutCache.java @@ -7,16 +7,13 @@ * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * 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. - * - * SPDX-License-Identifier: Apache-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.jena.sparql.service.enhancer.impl; diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerRewrite.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerRewrite.java new file mode 100644 index 00000000000..a1fafb11687 --- /dev/null +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceEnhancerRewrite.java @@ -0,0 +1,213 @@ +/* + * 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.jena.sparql.service.enhancer.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.query.Query; +import org.apache.jena.query.QueryFactory; +import org.apache.jena.query.Syntax; +import org.apache.jena.riot.Lang; +import org.apache.jena.riot.RDFParser; +import org.apache.jena.riot.system.PrefixMap; +import org.apache.jena.riot.system.PrefixMapFactory; +import org.apache.jena.shared.PrefixMapping; +import org.apache.jena.sparql.algebra.Algebra; +import org.apache.jena.sparql.algebra.Op; +import org.apache.jena.sparql.algebra.Table; +import org.apache.jena.sparql.algebra.op.OpService; +import org.apache.jena.sparql.core.DatasetGraph; +import org.apache.jena.sparql.core.Var; +import org.apache.jena.sparql.engine.binding.Binding; +import org.apache.jena.sparql.engine.binding.BindingFactory; +import org.apache.jena.sparql.exec.QueryExec; +import org.apache.jena.sparql.graph.GraphFactory; +import org.apache.jena.sparql.graph.PrefixMappingAdapter; +import org.apache.jena.sparql.service.enhancer.impl.BatchQueryRewriter.SubstitutionStrategy; +import org.apache.jena.sparql.service.enhancer.init.ServiceEnhancerInit; +import org.apache.jena.sparql.sse.SSE; +import org.apache.jena.sys.JenaSystem; + +public class TestServiceEnhancerRewrite { + // Ensure extensions are initialized + static { JenaSystem.init(); } + + // @Test + public void test01() { + ServiceEnhancerInit.init(); + + Query nonScopeRestrictedQuery = QueryFactory.create(""" + SELECT * { + BIND("foo" AS ?foo) + SERVICE { + SELECT ?bar { + BIND(?foo AS ?bar) + } + } + } + """); + + Query scopeRestrictedQuery = QueryFactory.create(""" + SELECT * { + BIND("foo" AS ?foo) + SERVICE { + SELECT ?foo ?bar { + BIND(?foo AS ?bar) + } + } + } + """); + + Binding b1 = MoreQueryExecUtils.evalToBinding(QueryExec.graph(GraphFactory.createDefaultGraph()).query(nonScopeRestrictedQuery).build(), ServiceEnhancerInit::wrapOptimizer); + Binding b2 = MoreQueryExecUtils.evalToBinding(QueryExec.graph(GraphFactory.createDefaultGraph()).query(scopeRestrictedQuery).build(), ServiceEnhancerInit::wrapOptimizer); + + // FIXME Validate output and remove sysouts! + System.out.println("TODO Finish " + this.getClass()); + System.out.println(b1); + System.out.println(b2); + +// BatchQueryRewriter rewriter = BatchQueryRewriterBuilder.from(new OpServiceInfo(op), Var.alloc("idx")) +// .setSequentialUnion(false) +// .setOrderRetainingUnion(false) +// .setOmitEndMarker(false) +// .setSubstitutionStrategy(SubstitutionStrategy.SUBSTITUTE) +// .build(); + + } + + // @Test + public void testFilterNotExists_01() { +// Op op1 = Algebra.compile(QueryFactory.create( +// """ +// SELECT * { +// VALUES (?this ?x) { } +// SERVICE { +// ?this ?p ?o +// FILTER NOT EXISTS { ?this ?x ?y . } +// } +// } +// """, Syntax.syntaxARQ)); + + + OpService op = (OpService)Algebra.compile(QueryFactory.create( + """ + SELECT * { + SERVICE { + ?this ?p ?o + FILTER NOT EXISTS { ?this ?x ?y . } + } + } + """, Syntax.syntaxARQ).getQueryPattern()); + + // FIXME There is a null element exception when analyzing not exists part + // Besides the warning message from, this might have unwanted consequences. + // The warning is generated by OpAsQuery which is used for the norm(alized) query. + // The normalized query is used in BatchQueryRewriter only for applying order by expressions + // - so variables in not exists shouldn't cause any problems - but still the warning should be handled. + OpServiceInfo opServiceInfo = new OpServiceInfo(op); + BatchQueryRewriter rewriter = BatchQueryRewriterBuilder.from(opServiceInfo, Var.alloc("idx")) + .setSequentialUnion(false) + .setOrderRetainingUnion(false) + .setOmitEndMarker(false) + .setSubstitutionStrategy(SubstitutionStrategy.SUBSTITUTE) + .build(); + + + Batch> batch = BatchImpl.forInteger(); + Var v1 = Var.alloc("this"); + Var v2 = Var.alloc("x"); + Binding b1 = BindingFactory.binding(v1, NodeFactory.createURI("urn:s1"), v2, NodeFactory.createURI("urn:p1")); + PartitionRequest pr1 = new PartitionRequest<>(0, b1, 0, Long.MAX_VALUE); + batch.put(0, pr1); + + Binding b2 = BindingFactory.binding(v1, NodeFactory.createURI("urn:s2"), v2, NodeFactory.createURI("urn:p2")); + PartitionRequest pr2 = new PartitionRequest<>(1, b2, 0, Long.MAX_VALUE); + batch.put(1, pr2); + + Op newOp = rewriter.rewrite(batch).getOp(); + + Op expectedOp = SSE.parseOp( + """ + (order ((asc ?idx)) + (union + (extend ((?idx 0)) + (filter (notexists (bgp (triple ?/y))) + (bgp (triple ?p ?o)))) + (union + (extend ((?idx 1)) + (filter (notexists (bgp (triple ?/y))) + (bgp (triple ?p ?o)))) + (extend ((?idx 1000000000)) + (table unit))))) + """); + String actualOpStr = newOp.toString(); + String expectedOpStr = expectedOp.toString(); + assertEquals(expectedOpStr, actualOpStr); + } + + /** + * This test case used to unexpectedly fail with the following error: + * - Binding already for ??P0 (different values) + * + * The reason was that the PathCompiler derived a new op with + * additional variables. The logic of {@code SERVICE } would then + * emit bindings with those extra variables. These variables would then + * clash with the lhs of the join. + * The fix was to only project the visible variables of the original op. + */ + @Test + public void testLoopWithOpPath() { + PrefixMap pm = PrefixMapFactory.create(Map.of("", "http://www.example.org/")); + PrefixMapping pmap = new PrefixMappingAdapter(pm); + + DatasetGraph dsg = RDFParser.create() + .fromString(""" + :a :p :b . + :b :p :c . + :c :p :d . + :d :p :e . + """).lang(Lang.TURTLE).prefixes(pm).toDatasetGraph(); + + Query query = QueryFactory.create(""" + PREFIX : + SELECT ?c ?e { + :a :p/:p ?c + SERVICE { + ?c :p/:p ?e + } + } + """); + + // This proces an algebra with two path patterns: + // (project (?c ?e) + // (join + // (path :a (seq :p :p) ?c) # PathCompiler introduces ??P0 + // (service + // (path ?c (seq :p :p) ?e)))) # PathCompiler introduces ??P0 - should no longer clash. + + Table expected = SSE.parseTable("(table (vars ?c ?e) (row (?c :c) (?e :e)))", pmap); + Table actual = QueryExec.dataset(dsg).query(query).table(); + assertEquals(expected, actual); + } +} diff --git a/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceOpts.java b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceOpts.java new file mode 100644 index 00000000000..2de46df5f50 --- /dev/null +++ b/jena-serviceenhancer/src/test/java/org/apache/jena/sparql/service/enhancer/impl/TestServiceOpts.java @@ -0,0 +1,73 @@ +/* + * 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.jena.sparql.service.enhancer.impl; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.AbstractMap.SimpleEntry; +import java.util.List; +import java.util.Map.Entry; + +import org.junit.jupiter.api.Test; + +import org.apache.jena.graph.Node; +import org.apache.jena.graph.NodeFactory; +import org.apache.jena.sparql.algebra.op.OpService; +import org.apache.jena.sparql.algebra.op.OpTable; + +/** Test cases for (un)parsing key-value pairs from IRI schemes */ +public class TestServiceOpts { + @Test + public void testParsing_01() { + assertEquals( + List.of("foo::bar", ":", "baz"), + ServiceOpts.parseEntriesRaw("foo::bar:baz")); + } + + @Test + public void testParsing_02() { + assertEquals( + List.of("::::foo::bar", ":", "baz::", ":"), + ServiceOpts.parseEntriesRaw("::::foo::bar:baz:::")); + } + + @Test + public void testParsing_03() { + assertEquals( + List.of("::", ":", "foo::bar", ":", "baz::", ":"), + ServiceOpts.parseEntriesRaw(":::foo::bar:baz:::")); + } + + @Test + public void testRoundTrip_01() { + String input = "::::foo::bar:baz:::"; + List> opts = ServiceOpts.parseEntries(input); + String unparsedStr = ServiceOpts.unparseEntries(opts); + assertEquals(input, unparsedStr); + } + + @Test + public void testServiceOpts_01() { + Node node = NodeFactory.createURI("cache:foo:bar:"); + OpService op = new OpService(node, OpTable.unit(), false); + ServiceOpts opts = ServiceOptsSE.getEffectiveService(op); + assertEquals("foo:bar:", opts.getTargetService().getService().getURI()); + assertEquals(List.of(new SimpleEntry<>("cache", null)), opts.getOptions()); + } +} diff --git a/jena-serviceenhancer/src/test/resources/linkedgeodata.sample.ttl b/jena-serviceenhancer/src/test/resources/linkedgeodata.sample.ttl new file mode 100644 index 00000000000..984a3e9c824 --- /dev/null +++ b/jena-serviceenhancer/src/test/resources/linkedgeodata.sample.ttl @@ -0,0 +1,3722 @@ +PREFIX rdf: +PREFIX owl: + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . + + + rdf:type owl:Class . + + + rdf:type . diff --git a/jena-serviceenhancer/src/test/resources/log4j2-test.properties b/jena-serviceenhancer/src/test/resources/log4j2-test.properties index aebc7a6f354..e4e39ea7d80 100644 --- a/jena-serviceenhancer/src/test/resources/log4j2-test.properties +++ b/jena-serviceenhancer/src/test/resources/log4j2-test.properties @@ -14,7 +14,7 @@ appender.console.layout.type = PatternLayout appender.console.layout.pattern = %d{HH:mm:ss} %-5p %-10c{1} :: %m%n #appender.console.layout.pattern = [%d{yyyy-MM-dd HH:mm:ss}] %-5p %-10c{1} :: %m%n -rootLogger.level = WARN +rootLogger.level = ${sys:logLevel:-warn} rootLogger.appenderRef.stdout.ref = OUT #logger.jena.name = org.apache.jena