You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@jena.apache.org by an...@apache.org on 2022/07/03 15:26:38 UTC

[jena] branch main updated: gh-1387: Bulk extension for service executor plugin system

This is an automated email from the ASF dual-hosted git repository.

andy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git


The following commit(s) were added to refs/heads/main by this push:
     new b2ed4de743 gh-1387: Bulk extension for service executor plugin system
     new db4a7505ef Merge pull request #1388 from Aklakan/gh-1387
b2ed4de743 is described below

commit b2ed4de743feeae48c82bcd4384341ea4b17282a
Author: Claus Stadler <Ra...@googlemail.com>
AuthorDate: Mon May 16 08:36:10 2022 +0200

    gh-1387: Bulk extension for service executor plugin system
---
 .../apache/jena/sparql/engine/QueryIterator.java   |   6 +-
 .../apache/jena/sparql/engine/main/OpExecutor.java |   3 +-
 .../engine/main/iterator/QueryIterService.java     |  19 +--
 .../jena/sparql/service/ServiceExecution.java      |  13 +-
 .../sparql/service/ServiceExecutorFactory.java     |  26 ++--
 .../sparql/service/ServiceExecutorRegistry.java    | 159 ++++++++++++++++++---
 .../ChainingServiceExecutorBulk.java}              |  20 ++-
 .../ServiceExecutorBulk.java}                      |  20 ++-
 .../bulk/ServiceExecutorBulkOverRegistry.java      |  79 ++++++++++
 .../service/bulk/ServiceExecutorBulkToSingle.java  |  53 +++++++
 .../ChainingServiceExecutor.java}                  |  12 +-
 .../single/ChainingServiceExecutorWrapper.java     |  59 ++++++++
 .../ServiceExecutor.java}                          |  10 +-
 .../ServiceExecutorDecorator.java}                 |  32 +++--
 .../sparql/service/single/ServiceExecutorHttp.java |  72 ++++++++++
 .../single/ServiceExecutorOverRegistry.java        |  69 +++++++++
 .../examples/service/CustomServiceExecutor.java    |  52 +++++--
 .../apache/jena/sparql/exec/http/TestService.java  |   8 +-
 .../test/service/TestCustomServiceExecutor.java    |  14 +-
 .../apache/jena/test/service/TestServiceExec.java  |   4 +-
 20 files changed, 604 insertions(+), 126 deletions(-)

diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/QueryIterator.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/QueryIterator.java
index 3b3821af03..a0f6003c86 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/QueryIterator.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/QueryIterator.java
@@ -18,15 +18,13 @@
 
 package org.apache.jena.sparql.engine;
 
-import java.util.Iterator ;
-
-import org.apache.jena.atlas.lib.Closeable ;
+import org.apache.jena.atlas.iterator.IteratorCloseable;
 import org.apache.jena.sparql.engine.binding.Binding ;
 import org.apache.jena.sparql.util.PrintSerializable ;
 
 /** Root of query iterators in ARQ. */
 
-public interface QueryIterator extends Closeable, Iterator<Binding>, PrintSerializable
+public interface QueryIterator extends IteratorCloseable<Binding>, PrintSerializable
 {
     /** Get next binding */
     public Binding nextBinding() ;
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/OpExecutor.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/OpExecutor.java
index 1e0785f5ac..fbd3e98fc0 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/OpExecutor.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/OpExecutor.java
@@ -45,6 +45,7 @@ import org.apache.jena.sparql.expr.Expr ;
 import org.apache.jena.sparql.expr.ExprList ;
 import org.apache.jena.sparql.procedure.ProcEval ;
 import org.apache.jena.sparql.procedure.Procedure ;
+import org.apache.jena.sparql.service.ServiceExecutorRegistry;
 
 /**
  * Turn an Op expression into an execution of QueryIterators.
@@ -305,7 +306,7 @@ public class OpExecutor
     }
 
     protected QueryIterator execute(OpService opService, QueryIterator input) {
-        return new QueryIterService(input, opService, execCxt) ;
+        return ServiceExecutorRegistry.exec(input, opService, execCxt);
     }
 
     // Quad form, "GRAPH ?g {}" Flip back to OpGraph.
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/iterator/QueryIterService.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/iterator/QueryIterService.java
index 8dd38171aa..bc276e3aef 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/iterator/QueryIterService.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/iterator/QueryIterService.java
@@ -31,12 +31,15 @@ import org.apache.jena.sparql.engine.iterator.QueryIterRepeatApply;
 import org.apache.jena.sparql.engine.iterator.QueryIterSingleton;
 import org.apache.jena.sparql.engine.main.QC ;
 import org.apache.jena.sparql.exec.http.Service;
-import org.apache.jena.sparql.service.ServiceExecution;
-import org.apache.jena.sparql.service.ServiceExecutorFactory;
 import org.apache.jena.sparql.service.ServiceExecutorRegistry;
+import org.apache.jena.sparql.service.single.ChainingServiceExecutor;
 import org.apache.jena.sparql.util.Context;
 
-
+/**
+ * This class continues to exist for compatibility with legacy service extensions.
+ * New code should register extensions at a {@link ServiceExecutorRegistry}.
+ */
+@Deprecated(since = "4.6.0")
 public class QueryIterService extends QueryIterRepeatApply
 {
     protected OpService opService ;
@@ -59,20 +62,21 @@ public class QueryIterService extends QueryIterRepeatApply
         ExecutionContext execCxt = getExecContext();
         Context cxt = execCxt.getContext();
         ServiceExecutorRegistry registry = ServiceExecutorRegistry.get(cxt);
-        ServiceExecution svcExec = null;
+        QueryIterator svcExec = null;
         OpService substitutedOp = (OpService)QC.substitute(opService, outerBinding);
 
         try {
             // ---- Find handler
             if ( registry != null ) {
-                for ( ServiceExecutorFactory factory : registry.getFactories() ) {
+                // FIXME This needs to be updated for chainable executors
+                for ( ChainingServiceExecutor factory : registry.getSingleChain() ) {
                     // Internal consistency check
                     if ( factory == null ) {
                         Log.warn(this, "SERVICE <" + opService.getService().toString() + ">: Null item in custom ServiceExecutionRegistry");
                         continue;
                     }
 
-                    svcExec = factory.createExecutor(substitutedOp, opService, outerBinding, execCxt);
+                    svcExec = factory.createExecution(substitutedOp, opService, outerBinding, execCxt, null);
                     if ( svcExec != null )
                         break;
                 }
@@ -81,8 +85,7 @@ public class QueryIterService extends QueryIterRepeatApply
             // ---- Execute
             if ( svcExec == null )
                 throw new QueryExecException("No SERVICE handler");
-            QueryIterator qIter = svcExec.exec();
-            qIter = QueryIter.makeTracked(qIter, getExecContext());
+            QueryIterator qIter = QueryIter.makeTracked(svcExec, getExecContext());
             // Need to put the outerBinding as parent to every binding of the service call.
             // There should be no variables in common because of the OpSubstitute.substitute
             return new QueryIterCommonParent(qIter, outerBinding, getExecContext());
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecution.java b/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecution.java
index 025ee921a9..e6d386169e 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecution.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecution.java
@@ -19,14 +19,15 @@
 package org.apache.jena.sparql.service;
 
 import org.apache.jena.sparql.engine.QueryIterator;
-import org.apache.jena.sparql.engine.main.iterator.QueryIterService;
 
-/** 
- * Execution of a SERVICE clause in the context of {@link QueryIterService} applying an input binding.
- * @see ServiceExecutorFactory
+/**
+ * Execution of a SERVICE clause in the context of {@link QueryIterService} applying an input binding.s
+ * @see ServiceExecutor
  * @see ServiceExecutorRegistry
- */  
+ *
+ * @deprecated Deprecated in favor of QueryIterators that initialize lazily
+ */
+@Deprecated(since = "4.6.0")
 public interface ServiceExecution {
     public QueryIterator exec();
 }
-
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java b/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java
index ee55d7a45b..0bdb01846b 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java
@@ -22,17 +22,21 @@ 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.engine.binding.Binding;
+import org.apache.jena.sparql.service.single.ChainingServiceExecutor;
+import org.apache.jena.sparql.service.single.ServiceExecutor;
 
-/**
- * Interface for custom handling of service execution requests.
- */
+/** Compatibility interface. Consider migrating legacy code to {@link ChainingServiceExecutor} or {@link ServiceExecutor} */
+@Deprecated(since = "4.6.0")
 @FunctionalInterface
-public interface ServiceExecutorFactory {
-    /**
-     * If this factory cannot handle the execution request then this method should return null.
-     * Otherwise, a {@link ServiceExecution} with the corresponding {@link QueryIterator} is returned.
-     *
-     * @return A QueryIterator if this factory can handle the request, or null otherwise.
-     */
-    public ServiceExecution createExecutor(OpService opExecute, OpService original, Binding binding, ExecutionContext execCxt);
+public interface ServiceExecutorFactory
+    extends ServiceExecutor
+{
+    @Override
+    default QueryIterator createExecution(OpService opExecute, OpService original, Binding binding, ExecutionContext execCxt) {
+        ServiceExecution svcExec = createExecutor(opExecute, original, binding, execCxt);
+        QueryIterator result = svcExec == null ? null : svcExec.exec();
+        return result;
+    }
+
+    ServiceExecution createExecutor(OpService opExecute, OpService original, Binding binding, ExecutionContext execCxt);
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorRegistry.java b/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorRegistry.java
index 71f4964abb..6390889bc9 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorRegistry.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorRegistry.java
@@ -19,18 +19,41 @@
 package org.apache.jena.sparql.service;
 
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Objects;
 
 import org.apache.jena.query.ARQ;
 import org.apache.jena.sparql.ARQConstants;
+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.bulk.ChainingServiceExecutorBulk;
+import org.apache.jena.sparql.service.bulk.ServiceExecutorBulk;
+import org.apache.jena.sparql.service.bulk.ServiceExecutorBulkOverRegistry;
+import org.apache.jena.sparql.service.single.ChainingServiceExecutor;
+import org.apache.jena.sparql.service.single.ChainingServiceExecutorWrapper;
+import org.apache.jena.sparql.service.single.ServiceExecutor;
+import org.apache.jena.sparql.service.single.ServiceExecutorHttp;
 import org.apache.jena.sparql.util.Context;
-import org.apache.jena.sparql.exec.http.*;
 
+/**
+ * Registry for service executors that can be extended with custom ones.
+ * Bulk and single (=non-bulk) executors are maintained in two separate lists.
+ *
+ * Default execution will always start with the bulk list first.
+ * Once that list is exhausted by means of all bulk executors having delegated the request,
+ * then the non-bulk ones will be considered.
+ * There is no need to explicitly register a bulk-to-non-bulk bridge.
+ */
 public class ServiceExecutorRegistry
 {
-    // A list of custom service executors which are tried in the given order
-    List<ServiceExecutorFactory> registry = new ArrayList<>();
+    // A list of bulk service executors which are tried in the given order
+    List<ChainingServiceExecutorBulk> bulkChain = new ArrayList<>();
+
+    // A list of single (non-bulk) service executors which are tried in the given order
+    // This list is only considered after after the bulk registry
+    List<ChainingServiceExecutor> singleChain = new ArrayList<>();
 
     public static ServiceExecutorRegistry standardRegistry()
     {
@@ -38,16 +61,25 @@ public class ServiceExecutorRegistry
         return reg ;
     }
 
-    /** A "call with SPARQL query" service execution factory. */
-    public static ServiceExecutorFactory httpService = (op, opx, binding, execCxt) -> ()->Service.exec(op, execCxt.getContext());
+    /** A "call with SPARQL query" service executor. */
+    public static ServiceExecutor httpService = new ServiceExecutorHttp();
+
+    /** Blindly adds the default executor(s); concretely adds the http executor */
+    public static void initWithDefaults(ServiceExecutorRegistry registry) {
+        registry.add(httpService);
+    }
 
     public static void init() {
         // Initialize if there is no registry already set
-        ServiceExecutorRegistry reg = new ServiceExecutorRegistry() ;
-        reg.add(httpService);
+        ServiceExecutorRegistry reg = new ServiceExecutorRegistry();
+        initWithDefaults(reg);
         set(ARQ.getContext(), reg) ;
     }
 
+    /**
+     * Return the global instance from the ARQ context; create that instance if needed.
+     * Never returns null.
+     */
     public static ServiceExecutorRegistry get()
     {
         // Initialize if there is no registry already set
@@ -61,6 +93,16 @@ public class ServiceExecutorRegistry
         return reg ;
     }
 
+    /** Return the registry from the given context if present; otherwise return the global one */
+    public static ServiceExecutorRegistry chooseRegistry(Context context) {
+        ServiceExecutorRegistry result = ServiceExecutorRegistry.get(context);
+        if (result == null) {
+            result = get();
+        }
+        return result;
+    }
+
+    /** Return the registry from the given context only; null if there is none */
     public static ServiceExecutorRegistry get(Context context)
     {
         if ( context == null )
@@ -76,29 +118,102 @@ public class ServiceExecutorRegistry
     public ServiceExecutorRegistry()
     {}
 
-    /** Create an independent copy of the registry */
-    public ServiceExecutorRegistry copy() {
-    	ServiceExecutorRegistry result = new ServiceExecutorRegistry();
-    	result.getFactories().addAll(getFactories());
-    	return result;
+    /*
+     * Non-bulk API
+     */
+
+    /** Prepend the given service executor as a link to the per-binding chain */
+    public ServiceExecutorRegistry addSingleLink(ChainingServiceExecutor f) {
+        Objects.requireNonNull(f) ;
+        singleChain.add(0, f) ;
+        return this;
+    }
+
+    /** Remove the given service executor from the per-binding chain */
+    public ServiceExecutorRegistry removeSingleLink(ChainingServiceExecutor f) {
+        singleChain.remove(f) ;
+        return this;
+    }
+
+    /** Wraps the given service executor as a chaining one and prepends it
+     *  to the non-bulk chain via {@link #addSingleLink(ChainingServiceExecutor)} */
+    public ServiceExecutorRegistry add(ServiceExecutor f) {
+        Objects.requireNonNull(f) ;
+        return addSingleLink(new ChainingServiceExecutorWrapper(f));
     }
 
-    /** Insert a service executor factory. Must not be null. */
-    public ServiceExecutorRegistry add(ServiceExecutorFactory f) {
+    /** Remove a given service executor - internally attempts to unwrap every chaining service executor */
+    public ServiceExecutorRegistry remove(ServiceExecutor f) {
+        Iterator<ChainingServiceExecutor> it = singleChain.iterator();
+        while (it.hasNext()) {
+            ChainingServiceExecutor cse = it.next();
+            if (cse instanceof ChainingServiceExecutorWrapper) {
+                ChainingServiceExecutorWrapper wrapper = (ChainingServiceExecutorWrapper)cse;
+                ServiceExecutor delegate = wrapper.getDelegate();
+                if (Objects.equals(delegate, f)) {
+                    it.remove();
+                }
+            }
+        }
+        return this;
+    }
+
+    /** Retrieve the actual list of per-binding executors; allows for re-ordering */
+    public List<ChainingServiceExecutor> getSingleChain() {
+        return singleChain;
+    }
+
+    /*
+     * Bulk API
+     */
+
+    /** Add a chaining bulk executor as a link to the executor chain */
+    public ServiceExecutorRegistry addBulkLink(ChainingServiceExecutorBulk f) {
         Objects.requireNonNull(f) ;
-        registry.add(0, f) ;
+        bulkChain.add(0, f) ;
         return this;
     }
 
-    /** Remove the given service executor factory. */
-    public ServiceExecutorRegistry remove(ServiceExecutorFactory f) {
-        registry.remove(f) ;
+    /** Remove the given service executor */
+    public ServiceExecutorRegistry removeBulkLink(ChainingServiceExecutorBulk f) {
+        bulkChain.remove(f) ;
         return this;
     }
 
-    /** Retrieve the actual list of factories; allows for re-ordering */
-    public List<ServiceExecutorFactory> getFactories() {
-        return registry;
+    /** Retrieve the actual list of bulk executors; allows for re-ordering */
+    public List<ChainingServiceExecutorBulk> getBulkChain() {
+        return bulkChain;
     }
 
-}
\ No newline at end of file
+    /*
+     * Utility
+     */
+
+    /** Create an independent copy of the registry */
+    public ServiceExecutorRegistry copy() {
+        ServiceExecutorRegistry result = new ServiceExecutorRegistry();
+        result.getSingleChain().addAll(getSingleChain());
+        result.getBulkChain().addAll(getBulkChain());
+        return result;
+    }
+
+    /** Return a copy of the registry in the context (if present) or a fresh instance */
+    public ServiceExecutorRegistry copyFrom(Context cxt) {
+        ServiceExecutorRegistry tmp = ServiceExecutorRegistry.get(cxt);
+        ServiceExecutorRegistry result = tmp == null ? new ServiceExecutorRegistry() : tmp.copy();
+        return result;
+    }
+
+    /*
+     * Execution
+     */
+
+    /** Execute an OpService w.r.t. the execCxt's service executor registry */
+    public static QueryIterator exec(QueryIterator input, OpService opService, ExecutionContext execCxt) {
+        Context cxt = execCxt.getContext();
+        ServiceExecutorRegistry registry = ServiceExecutorRegistry.chooseRegistry(cxt);
+        ServiceExecutorBulk serviceExecutor = new ServiceExecutorBulkOverRegistry(registry);
+        QueryIterator qIter = serviceExecutor.createExecution(opService, input, execCxt);
+        return qIter;
+    }
+}
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java b/jena-arq/src/main/java/org/apache/jena/sparql/service/bulk/ChainingServiceExecutorBulk.java
similarity index 59%
copy from jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java
copy to jena-arq/src/main/java/org/apache/jena/sparql/service/bulk/ChainingServiceExecutorBulk.java
index ee55d7a45b..36bef79a3f 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/service/bulk/ChainingServiceExecutorBulk.java
@@ -16,23 +16,21 @@
  * limitations under the License.
  */
 
-package org.apache.jena.sparql.service;
+package org.apache.jena.sparql.service.bulk;
 
 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.engine.binding.Binding;
 
-/**
- * Interface for custom handling of service execution requests.
- */
-@FunctionalInterface
-public interface ServiceExecutorFactory {
+/** Interface for custom service execution extensions that handle
+ *  the iterator over the input bindings themselves */
+public interface ChainingServiceExecutorBulk {
     /**
-     * If this factory cannot handle the execution request then this method should return null.
-     * Otherwise, a {@link ServiceExecution} with the corresponding {@link QueryIterator} is returned.
+     * If this executor cannot handle the createExecution request then it should delegate
+     * to the chain's @{code createExecution} method and return its result.
+     * In any case, a {@link QueryIterator} needs to be returned.
      *
-     * @return A QueryIterator if this factory can handle the request, or null otherwise.
+     * @return A non-null {@link QueryIterator} for the execution of the given OpService expression.
      */
-    public ServiceExecution createExecutor(OpService opExecute, OpService original, Binding binding, ExecutionContext execCxt);
+    public QueryIterator createExecution(OpService opService, QueryIterator input, ExecutionContext execCxt, ServiceExecutorBulk chain);
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java b/jena-arq/src/main/java/org/apache/jena/sparql/service/bulk/ServiceExecutorBulk.java
similarity index 59%
copy from jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java
copy to jena-arq/src/main/java/org/apache/jena/sparql/service/bulk/ServiceExecutorBulk.java
index ee55d7a45b..d19ccc52d1 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/service/bulk/ServiceExecutorBulk.java
@@ -16,23 +16,19 @@
  * limitations under the License.
  */
 
-package org.apache.jena.sparql.service;
+package org.apache.jena.sparql.service.bulk;
 
 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.engine.binding.Binding;
+import org.apache.jena.sparql.service.ServiceExecutorRegistry;
 
 /**
- * Interface for custom handling of service execution requests.
+ * Interface for abstracting {@link OpService} execution.
+ *
+ * Custom extensions should provide implementations of {@link ChainingServiceExecutorBulk}
+ * and register them with {@link ServiceExecutorRegistry#addBulkLink(ChainingServiceExecutorBulk)}.
  */
-@FunctionalInterface
-public interface ServiceExecutorFactory {
-    /**
-     * If this factory cannot handle the execution request then this method should return null.
-     * Otherwise, a {@link ServiceExecution} with the corresponding {@link QueryIterator} is returned.
-     *
-     * @return A QueryIterator if this factory can handle the request, or null otherwise.
-     */
-    public ServiceExecution createExecutor(OpService opExecute, OpService original, Binding binding, ExecutionContext execCxt);
+public interface ServiceExecutorBulk {
+    public QueryIterator createExecution(OpService opService, QueryIterator input, ExecutionContext execCxt);
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/service/bulk/ServiceExecutorBulkOverRegistry.java b/jena-arq/src/main/java/org/apache/jena/sparql/service/bulk/ServiceExecutorBulkOverRegistry.java
new file mode 100644
index 0000000000..300a54e01b
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/service/bulk/ServiceExecutorBulkOverRegistry.java
@@ -0,0 +1,79 @@
+/*
+ * 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.bulk;
+
+import java.util.List;
+
+import org.apache.jena.query.QueryException;
+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.ServiceExecutorRegistry;
+import org.apache.jena.sparql.service.single.ServiceExecutor;
+import org.apache.jena.sparql.service.single.ServiceExecutorOverRegistry;
+
+/**
+ * Factory for service executions w.r.t. a {@link ServiceExecutorRegistry}.
+ * The {@link #createExecution(OpService, QueryIterator, ExecutionContext)} method
+ * delegates the request to all executors in order of their registration.
+ */
+public class ServiceExecutorBulkOverRegistry
+    implements ServiceExecutorBulk
+{
+    protected ServiceExecutorRegistry registry;
+
+    /** Position in the chain */
+    protected int pos;
+
+    public ServiceExecutorBulkOverRegistry(ServiceExecutorRegistry registry) {
+        this(registry, 0);
+    }
+
+    public ServiceExecutorBulkOverRegistry(ServiceExecutorRegistry registry, int pos) {
+        super();
+        this.registry = registry;
+        this.pos = pos;
+    }
+
+    @Override
+    public QueryIterator createExecution(OpService opService, QueryIterator input, ExecutionContext execCxt) {
+        if (registry == null) {
+            throw new QueryException("No service executor registry configured");
+        }
+
+        QueryIterator result;
+
+        List<ChainingServiceExecutorBulk> factories = registry.getBulkChain();
+        int n = factories.size();
+        if (pos >= n) {
+            // Chain to the single registry
+            ServiceExecutor singleExecutor = new ServiceExecutorOverRegistry(registry);
+            ServiceExecutorBulk bridge = new ServiceExecutorBulkToSingle(singleExecutor);
+            result = bridge.createExecution(opService, input, execCxt);
+
+            // Alternatively we could require for the bridge to be explicitly registered
+            // throw new QueryException("No more elements in service executor chain (pos=" + pos + ", chain size=" + n + ")");
+        } else {
+            ChainingServiceExecutorBulk factory = factories.get(pos);
+            ServiceExecutorBulk next = new ServiceExecutorBulkOverRegistry(registry, pos + 1);
+            result = factory.createExecution(opService, input, execCxt, next);
+        }
+        return result;
+    }
+}
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/service/bulk/ServiceExecutorBulkToSingle.java b/jena-arq/src/main/java/org/apache/jena/sparql/service/bulk/ServiceExecutorBulkToSingle.java
new file mode 100644
index 0000000000..fc3bd61719
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/service/bulk/ServiceExecutorBulkToSingle.java
@@ -0,0 +1,53 @@
+/*
+ * 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.bulk;
+
+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.engine.binding.Binding;
+import org.apache.jena.sparql.engine.iterator.QueryIterRepeatApply;
+import org.apache.jena.sparql.engine.main.QC;
+import org.apache.jena.sparql.service.single.ServiceExecutor;
+
+/** Bridge from bulk to individual binding level*/
+public class ServiceExecutorBulkToSingle
+    implements ServiceExecutorBulk
+{
+    protected ServiceExecutor delegate;
+
+    public ServiceExecutorBulkToSingle(ServiceExecutor delegate) {
+        super();
+        this.delegate = delegate;
+    }
+
+    @Override
+    public QueryIterator createExecution(OpService original, QueryIterator input,
+            ExecutionContext execCxt) {
+
+        return new QueryIterRepeatApply(input, execCxt) {
+            @Override
+            protected QueryIterator nextStage(Binding binding) {
+                OpService opExecute = (OpService)QC.substitute(original, binding);
+                QueryIterator qIter = delegate.createExecution(opExecute, original, binding, execCxt);
+                return qIter;
+            }
+        };
+    }
+}
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java b/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ChainingServiceExecutor.java
similarity index 75%
copy from jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java
copy to jena-arq/src/main/java/org/apache/jena/sparql/service/single/ChainingServiceExecutor.java
index ee55d7a45b..b3c8aac95a 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ChainingServiceExecutor.java
@@ -16,23 +16,19 @@
  * limitations under the License.
  */
 
-package org.apache.jena.sparql.service;
+package org.apache.jena.sparql.service.single;
 
 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.engine.binding.Binding;
 
-/**
- * Interface for custom handling of service execution requests.
- */
-@FunctionalInterface
-public interface ServiceExecutorFactory {
+public interface ChainingServiceExecutor {
     /**
      * If this factory cannot handle the execution request then this method should return null.
-     * Otherwise, a {@link ServiceExecution} with the corresponding {@link QueryIterator} is returned.
+     * Otherwise, a {@link QueryIterator} is returned.
      *
      * @return A QueryIterator if this factory can handle the request, or null otherwise.
      */
-    public ServiceExecution createExecutor(OpService opExecute, OpService original, Binding binding, ExecutionContext execCxt);
+    public QueryIterator createExecution(OpService opExecute, OpService opOriginal, Binding binding, ExecutionContext execCxt, ServiceExecutor chain);
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ChainingServiceExecutorWrapper.java b/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ChainingServiceExecutorWrapper.java
new file mode 100644
index 0000000000..f27c0770b2
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ChainingServiceExecutorWrapper.java
@@ -0,0 +1,59 @@
+/*
+ * 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.single;
+
+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.engine.binding.Binding;
+import org.apache.jena.sparql.service.ServiceExecutorRegistry;
+
+/**
+ * Turns a ServiceExecutor into a chaining one.
+ * Mainly used by {@link ServiceExecutorRegistry} for wrapping
+ * non-chaining service executors.
+ * If the executor returns null then the next link in the chain will be tried.
+ */
+public class ChainingServiceExecutorWrapper
+    implements ChainingServiceExecutor
+{
+    protected ServiceExecutor executor;
+
+    public ChainingServiceExecutorWrapper(ServiceExecutor executor) {
+        super();
+        this.executor = executor;
+    }
+
+    public ServiceExecutor getDelegate() {
+        return executor;
+    }
+
+    @Override
+    public QueryIterator createExecution(OpService opExecute, OpService opOriginal, Binding binding,
+            ExecutionContext execCxt, ServiceExecutor chain) {
+
+        QueryIterator qIter = executor.createExecution(opExecute, opOriginal, binding, execCxt);
+        QueryIterator result = qIter != null
+                ? qIter
+                : chain.createExecution(opExecute, opOriginal, binding, execCxt);
+
+        return result;
+    }
+
+}
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java b/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ServiceExecutor.java
similarity index 76%
copy from jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java
copy to jena-arq/src/main/java/org/apache/jena/sparql/service/single/ServiceExecutor.java
index ee55d7a45b..c43f805e25 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ServiceExecutor.java
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-package org.apache.jena.sparql.service;
+package org.apache.jena.sparql.service.single;
 
 import org.apache.jena.sparql.algebra.op.OpService;
 import org.apache.jena.sparql.engine.ExecutionContext;
@@ -24,15 +24,15 @@ import org.apache.jena.sparql.engine.QueryIterator;
 import org.apache.jena.sparql.engine.binding.Binding;
 
 /**
- * Interface for custom handling of service execution requests.
+ * Interface for handling service execution requests on a per-binding level.
  */
 @FunctionalInterface
-public interface ServiceExecutorFactory {
+public interface ServiceExecutor {
     /**
      * If this factory cannot handle the execution request then this method should return null.
-     * Otherwise, a {@link ServiceExecution} with the corresponding {@link QueryIterator} is returned.
+     * Otherwise, a {@link QueryIterator} is returned.
      *
      * @return A QueryIterator if this factory can handle the request, or null otherwise.
      */
-    public ServiceExecution createExecutor(OpService opExecute, OpService original, Binding binding, ExecutionContext execCxt);
+    public QueryIterator createExecution(OpService opExecute, OpService original, Binding binding, ExecutionContext execCxt);
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java b/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ServiceExecutorDecorator.java
similarity index 58%
copy from jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java
copy to jena-arq/src/main/java/org/apache/jena/sparql/service/single/ServiceExecutorDecorator.java
index ee55d7a45b..f5db12bf7e 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/service/ServiceExecutorFactory.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ServiceExecutorDecorator.java
@@ -16,23 +16,29 @@
  * limitations under the License.
  */
 
-package org.apache.jena.sparql.service;
+package org.apache.jena.sparql.service.single;
 
 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.engine.binding.Binding;
 
-/**
- * Interface for custom handling of service execution requests.
- */
-@FunctionalInterface
-public interface ServiceExecutorFactory {
-    /**
-     * If this factory cannot handle the execution request then this method should return null.
-     * Otherwise, a {@link ServiceExecution} with the corresponding {@link QueryIterator} is returned.
-     *
-     * @return A QueryIterator if this factory can handle the request, or null otherwise.
-     */
-    public ServiceExecution createExecutor(OpService opExecute, OpService original, Binding binding, ExecutionContext execCxt);
+/** Form a service executor from a base service executor and a 'chain' that acts as a decorator */
+public class ServiceExecutorDecorator
+    implements ServiceExecutor
+{
+    protected ServiceExecutor base;
+    protected ChainingServiceExecutor decorator;
+
+    public ServiceExecutorDecorator(ServiceExecutor base, ChainingServiceExecutor decorator) {
+        super();
+        this.base = base;
+        this.decorator = decorator;
+    }
+
+    @Override
+    public QueryIterator createExecution(OpService opExecute, OpService original, Binding binding,
+            ExecutionContext execCxt) {
+        return decorator.createExecution(opExecute, original, binding, execCxt, base);
+    }
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ServiceExecutorHttp.java b/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ServiceExecutorHttp.java
new file mode 100644
index 0000000000..89e92e69c8
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ServiceExecutorHttp.java
@@ -0,0 +1,72 @@
+/*
+ * 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.single;
+
+import org.apache.jena.atlas.logging.Log;
+import org.apache.jena.query.QueryExecException;
+import org.apache.jena.riot.out.NodeFmtLib;
+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.engine.binding.Binding;
+import org.apache.jena.sparql.engine.iterator.QueryIter;
+import org.apache.jena.sparql.engine.iterator.QueryIterCommonParent;
+import org.apache.jena.sparql.engine.iterator.QueryIterSingleton;
+import org.apache.jena.sparql.exec.http.Service;
+import org.apache.jena.sparql.util.Context;
+
+/** The default HTTP service executor implementation */
+public class ServiceExecutorHttp
+    implements ServiceExecutor
+{
+    @Override
+    public QueryIterator createExecution(OpService opExecute, OpService opOriginal, Binding binding,
+            ExecutionContext execCxt) {
+
+        Context context = execCxt.getContext();
+        if ( context.isFalse(Service.httpServiceAllowed) )
+            throw new QueryExecException("SERVICE not allowed") ;
+        // Old name.
+        if ( context.isFalse(Service.serviceAllowed) )
+            throw new QueryExecException("SERVICE not allowed") ;
+
+        boolean silent = opExecute.getSilent();
+
+        try {
+            QueryIterator qIter = Service.exec(opExecute, context);
+
+            // ---- Execute
+            if ( qIter == null )
+                throw new QueryExecException("No SERVICE handler");
+
+            qIter = QueryIter.makeTracked(qIter, execCxt);
+            // Need to put the outerBinding as parent to every binding of the service call.
+            // There should be no variables in common because of the OpSubstitute.substitute
+            return new QueryIterCommonParent(qIter, binding, execCxt);
+        } catch (RuntimeException ex) {
+            if ( silent ) {
+                Log.warn(this, "SERVICE " + NodeFmtLib.strTTL(opExecute.getService()) + " : " + ex.getMessage());
+                // Return the input
+                return QueryIterSingleton.create(binding, execCxt);
+
+            }
+            throw ex;
+        }
+    }
+}
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ServiceExecutorOverRegistry.java b/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ServiceExecutorOverRegistry.java
new file mode 100644
index 0000000000..d9d55f01c1
--- /dev/null
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/service/single/ServiceExecutorOverRegistry.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
+ *
+ *     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.single;
+
+import java.util.List;
+
+import org.apache.jena.query.QueryException;
+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.engine.binding.Binding;
+import org.apache.jena.sparql.engine.iterator.QueryIterRoot;
+import org.apache.jena.sparql.service.ServiceExecutorRegistry;
+
+/** Abstraction of a registry's single chain as a service executor */
+public class ServiceExecutorOverRegistry
+    implements ServiceExecutor
+{
+    protected ServiceExecutorRegistry registry;
+
+    /** Position in the chain */
+    protected int pos;
+
+    public ServiceExecutorOverRegistry(ServiceExecutorRegistry registry) {
+        this(registry, 0);
+    }
+
+    public ServiceExecutorOverRegistry(ServiceExecutorRegistry registry, int pos) {
+        super();
+        this.registry = registry;
+        this.pos = pos;
+    }
+
+    @Override
+    public QueryIterator createExecution(OpService opExecute, OpService original, Binding binding, ExecutionContext execCxt) {
+        List<ChainingServiceExecutor> factories = registry.getSingleChain();
+        int n = factories.size();
+        if (pos >= n) {
+            if (opExecute.getSilent()) {
+                return QueryIterRoot.create(execCxt);
+            } else {
+                throw new QueryException("No more elements in service executor chain (pos=" + pos + ", chain size=" + n + ")");
+            }
+        }
+
+        ChainingServiceExecutor factory = factories.get(pos);
+
+        ServiceExecutor next = new ServiceExecutorOverRegistry(registry, pos + 1);
+        QueryIterator result = factory.createExecution(opExecute, original, binding, execCxt, next);
+
+        return result;
+    }
+}
diff --git a/jena-examples/src/main/java/arq/examples/service/CustomServiceExecutor.java b/jena-examples/src/main/java/arq/examples/service/CustomServiceExecutor.java
index da95fd5596..cc21c622f4 100644
--- a/jena-examples/src/main/java/arq/examples/service/CustomServiceExecutor.java
+++ b/jena-examples/src/main/java/arq/examples/service/CustomServiceExecutor.java
@@ -18,25 +18,24 @@
 
 package arq.examples.service;
 
-import java.util.Collections;
-
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.NodeFactory;
 import org.apache.jena.query.ARQ;
 import org.apache.jena.query.Dataset;
 import org.apache.jena.query.DatasetFactory;
 import org.apache.jena.sparql.algebra.op.OpService;
-import org.apache.jena.sparql.engine.iterator.QueryIterPlainWrapper;
+import org.apache.jena.sparql.engine.iterator.QueryIterRoot;
 import org.apache.jena.sparql.exec.QueryExec;
 import org.apache.jena.sparql.exec.QueryExecDatasetBuilder;
-import org.apache.jena.sparql.service.ServiceExecutorFactory;
 import org.apache.jena.sparql.service.ServiceExecutorRegistry;
+import org.apache.jena.sparql.service.single.ChainingServiceExecutor;
+import org.apache.jena.sparql.service.single.ServiceExecutor;
 import org.apache.jena.sparql.util.Context;
 import org.apache.jena.sparql.util.QueryExecUtils;
 
 public class CustomServiceExecutor {
 
-	/** Query for resources having the label "Apache Jena"en */
+    /** Query for resources having the label "Apache Jena"en */
     public static final String QUERY_STR = String.join("\n",
             "PREFIX wd: <http://www.wikidata.org/entity/>",
             "PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>",
@@ -49,11 +48,15 @@ public class CustomServiceExecutor {
             "  }",
             "}");
 
+    public static final Node WIKIDATA = NodeFactory.createURI("http://query.wikidata.org/sparql");
+    public static final Node DBPEDIA = NodeFactory.createURI("http://dbpedia.org/sparql");
+
     public static void main(String[] args) {
 
         Dataset dataset = DatasetFactory.empty();
 
         conventionalExec(dataset);
+        relayWikidataToDBpediaChaining(dataset);
         relayWikidataToDBpedia(dataset);
         suppressRemoteRequests(dataset);
     }
@@ -71,21 +74,43 @@ public class CustomServiceExecutor {
          */
     }
 
-    /** Relay requests for Wikidata to DBpedia instead */
-    public static void relayWikidataToDBpedia(Dataset dataset) {
-        Node WIKIDATA = NodeFactory.createURI("http://query.wikidata.org/sparql");
-        Node DBPEDIA = NodeFactory.createURI("http://dbpedia.org/sparql");
+    /** Relay requests for Wikidata to DBpedia instead. Modern variant using chaining. */
+    public static void relayWikidataToDBpediaChaining(Dataset dataset) {
+        ChainingServiceExecutor relaySef = (opExecute, original, binding, execCxt, chain) -> {
+            if (opExecute.getService().equals(WIKIDATA)) {
+                opExecute = new OpService(DBPEDIA, opExecute.getSubOp(), opExecute.getSilent());
+            }
+            return chain.createExecution(opExecute, original, binding, execCxt);
+        };
+
+        Context cxt = ARQ.getContext().copy();
+        ServiceExecutorRegistry registry = ServiceExecutorRegistry.get(cxt).copy();
+        registry.addSingleLink(relaySef);
+
+        ServiceExecutorRegistry.set(cxt, registry);
+        execQueryAndShowResult(dataset, QUERY_STR, cxt);
+
+        /*
+         * -------------------
+         * | s               |
+         * ===================
+         * | dbr:Apache_Jena |
+         * -------------------
+         */
+    }
 
-        ServiceExecutorFactory relaySef = (opExecute, original, binding, execCxt) -> {
+    /** Relay requests for Wikidata to DBpedia instead. Non-chaining legacy variant. */
+    public static void relayWikidataToDBpedia(Dataset dataset) {
+        ServiceExecutor relaySef = (opExecute, original, binding, execCxt) -> {
                 if (opExecute.getService().equals(WIKIDATA)) {
                     opExecute = new OpService(DBPEDIA, opExecute.getSubOp(), opExecute.getSilent());
-                    return ServiceExecutorRegistry.httpService.createExecutor(opExecute, original, binding, execCxt);
+                    return ServiceExecutorRegistry.httpService.createExecution(opExecute, original, binding, execCxt);
                 }
                 return null;
             };
 
         Context cxt = ARQ.getContext().copy();
-        ServiceExecutorRegistry registry = new ServiceExecutorRegistry();
+        ServiceExecutorRegistry registry = ServiceExecutorRegistry.get(cxt).copy();
         registry.add(relaySef);
 
         ServiceExecutorRegistry.set(cxt, registry);
@@ -102,8 +127,7 @@ public class CustomServiceExecutor {
 
     /** Suppress remote requests - make any SERVICE request return the input binding */
     public static void suppressRemoteRequests(Dataset dataset) {
-        ServiceExecutorFactory noop = (opExecute, original, binding, execCxt) ->
-            () -> QueryIterPlainWrapper.create(Collections.singleton(binding).iterator());
+        ServiceExecutor noop = (opExecute, original, binding, execCxt) -> QueryIterRoot.create(execCxt);
 
         Context cxt = ARQ.getContext().copy();
         ServiceExecutorRegistry registry = new ServiceExecutorRegistry();
diff --git a/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestService.java b/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestService.java
index 0d27915d94..eb29b989d6 100644
--- a/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestService.java
+++ b/jena-integration-tests/src/test/java/org/apache/jena/sparql/exec/http/TestService.java
@@ -46,7 +46,7 @@ import org.apache.jena.sparql.core.DatasetGraphZero;
 import org.apache.jena.sparql.engine.QueryIterator;
 import org.apache.jena.sparql.engine.binding.Binding;
 import org.apache.jena.sparql.engine.http.QueryExceptionHTTP;
-import org.apache.jena.sparql.engine.main.iterator.QueryIterService;
+import org.apache.jena.sparql.service.ServiceExecutorRegistry;
 import org.apache.jena.sparql.exec.QueryExec;
 import org.apache.jena.sparql.exec.RowSet;
 import org.apache.jena.sparql.sse.SSE;
@@ -108,7 +108,7 @@ public class TestService {
     }
 
     // Remember the initial settings.
-    static String logLevelQueryIterService = LogCtl.getLevel(QueryIterService.class);
+    static String logLevelQueryIterService = LogCtl.getLevel(ServiceExecutorRegistry.class);
     static String logLevelFuseki = LogCtl.getLevel(Fuseki.class);
 
     @BeforeClass public static void beforeClass() {
@@ -185,7 +185,7 @@ public class TestService {
     }
 
     @Test public void service_query_silent_no_service() {
-        logOnlyErrors(QueryIterService.class, ()->{
+        logOnlyErrors(ServiceExecutorRegistry.class, ()->{
             DatasetGraph dsg = env.dsg();
             String queryString = "SELECT * { SERVICE SILENT <"+SERVICE+"JUNK> { VALUES ?X { 1 2 } }} ";
             try ( RDFLink link = RDFLinkFactory.connect(localDataset()) ) {
@@ -201,7 +201,7 @@ public class TestService {
     }
 
     @Test public void service_query_silent_nosite() {
-        logOnlyErrors(QueryIterService.class, ()->{
+        logOnlyErrors(ServiceExecutorRegistry.class, ()->{
             DatasetGraph dsg = env.dsg();
             String queryString = "SELECT * { SERVICE SILENT <http://nosuchsite/> { VALUES ?X { 1 2 } }} ";
             try ( RDFLink link = RDFLinkFactory.connect(localDataset()) ) {
diff --git a/jena-integration-tests/src/test/java/org/apache/jena/test/service/TestCustomServiceExecutor.java b/jena-integration-tests/src/test/java/org/apache/jena/test/service/TestCustomServiceExecutor.java
index 97b01f401e..144547f6b9 100644
--- a/jena-integration-tests/src/test/java/org/apache/jena/test/service/TestCustomServiceExecutor.java
+++ b/jena-integration-tests/src/test/java/org/apache/jena/test/service/TestCustomServiceExecutor.java
@@ -32,10 +32,9 @@ import org.apache.jena.riot.resultset.ResultSetLang;
 import org.apache.jena.sparql.algebra.Table;
 import org.apache.jena.sparql.core.Var;
 import org.apache.jena.sparql.engine.binding.Binding;
-import org.apache.jena.sparql.engine.main.iterator.QueryIterService;
 import org.apache.jena.sparql.resultset.ResultSetCompare;
-import org.apache.jena.sparql.service.ServiceExecutorFactory;
 import org.apache.jena.sparql.service.ServiceExecutorRegistry;
+import org.apache.jena.sparql.service.single.ServiceExecutor;
 import org.apache.jena.sparql.sse.SSE;
 import org.junit.Assert;
 import org.junit.Test;
@@ -46,15 +45,16 @@ public class TestCustomServiceExecutor {
 
     /** A custom service factory that yields the above table for any request
      *  to urn:customService */
-    static ServiceExecutorFactory factory = (op, opOriginal, binding, execCxt) ->
+    static ServiceExecutor factory = (op, opOriginal, binding, execCxt) ->
         op.getService().getURI().equals("urn:customService")
-            ? ()->table.iterator(execCxt)
+            ? table.iterator(execCxt)
             : null;
 
     static ServiceExecutorRegistry customRegistry = new ServiceExecutorRegistry().add(factory);
 
     @Test
     public void testGlobalServiceExecutorRegistry() {
+        int sizeBefore = ServiceExecutorRegistry.get().getSingleChain().size();
         ServiceExecutorRegistry.get().add(factory);
 
         try {
@@ -62,6 +62,10 @@ public class TestCustomServiceExecutor {
         } finally {
             // Better eventually remove the global registration
             ServiceExecutorRegistry.get().remove(factory);
+            int sizeAfter = ServiceExecutorRegistry.get().getSingleChain().size();
+
+            // Perform a sanity check
+            Assert.assertEquals("Removal of a registration failed", sizeBefore, sizeAfter);
         }
     }
 
@@ -98,7 +102,7 @@ public class TestCustomServiceExecutor {
      */
     @Test
     public void testIllegalServiceIri2() {
-        Class<?> logClass = QueryIterService.class;
+        Class<?> logClass = ServiceExecutorRegistry.class;
         String logLevel = LogCtl.getLevel(logClass);
         try {
             LogCtl.setLevel(logClass, "ERROR");
diff --git a/jena-integration-tests/src/test/java/org/apache/jena/test/service/TestServiceExec.java b/jena-integration-tests/src/test/java/org/apache/jena/test/service/TestServiceExec.java
index cc5d30c610..0077034429 100644
--- a/jena-integration-tests/src/test/java/org/apache/jena/test/service/TestServiceExec.java
+++ b/jena-integration-tests/src/test/java/org/apache/jena/test/service/TestServiceExec.java
@@ -27,7 +27,7 @@ import org.apache.jena.query.*;
 import org.apache.jena.sparql.core.DatasetGraph;
 import org.apache.jena.sparql.core.DatasetGraphFactory;
 import org.apache.jena.sparql.engine.http.QueryExceptionHTTP;
-import org.apache.jena.sparql.engine.main.iterator.QueryIterService;
+import org.apache.jena.sparql.service.ServiceExecutorRegistry;
 import org.apache.jena.sparql.sse.SSE;
 import org.junit.BeforeClass;
 import org.junit.Test;
@@ -72,7 +72,7 @@ public class TestServiceExec {
 
     @Test
     public void service_exec_3() {
-        Class<?> logClass = QueryIterService.class;
+        Class<?> logClass = ServiceExecutorRegistry.class;
         String logLevel = LogCtl.getLevel(logClass);
         try {
             LogCtl.setLevel(logClass, "ERROR");