Repository: apache/jena
Pull request #3047: GH-3044: HashJoin iterator construction no longer eagerly builds hash probe table.
Author: login: Aklakan
Created: 2025-03-05T22:15:56Z | Merged: 2025-03-25T09:20:56Z

GitHub issue resolved #3044 
Pull request Description: Timeouts in QueryExecDataset were postponed until a so-far eager hash probe table construction completed because of locking. This PR proposes to initialize the hash probe table lazily such that the iterator construction becomes cheap and the lock is released quickly.

I added a test case that fails without the proposed fix.

----

 - [x] Tests are included.
 - (Documentation change and updates are provided for the [Apache Jena website](https://github.com/apache/jena-site/))
 - [x] Commits have been squashed to remove intermediate development commit messages.
 - [x] Key commit messages start with the issue number (GH-xxxx)

By submitting this pull request, I acknowledge that I am making a contribution to the Apache Software Foundation under the terms and conditions of the [Contributor's Agreement](https://www.apache.org/licenses/contributor-agreements.html).

----

See the [Apache Jena "Contributing" guide](https://github.com/apache/jena/blob/main/CONTRIBUTING.md).


Linked issue #3044: timeout parameter is not always respected by fuseki server
### Version

5.3.0

### What happened?

Using the Python _requests_ client, I am running sequentially a list of diverse SPARQL queries on a fuseki endpoint at localhost:3030, using POST request and a timeout parameter set to 10 s. For the few first lengthy query in the list, the timeout works as expected, i.e. the request terminates after approx 10 s and with a 503 status code. However at some point the timeout of subsequent queries is ignored, i.e.  the queries executing correctly and completely. 

As I suspect that this problem might be linked to the fuseki HTTP server, I have not investigated the content of the queries that are not timing out. Should I?

Thanks in advance,

  Marco

### Relevant output and stacktrace

```shell

```

### Are you interested in making a pull request?

None
Linked issue comments: user: login: afs
created_at: 2025-03-04T16:28:39Z
body: @mpagni12 -- theer isn't enough to recreate the situation from the description.

Could you provide the fuseki server log file for the relevant first request (timeouts) and later (non-timeouts). If that can be "verbose"  (server "`-v`") that would be best.

What is the storage? TDB2? Any inference?

> I have not investigated the content of the queries that are not timing out. Should I?

It does seem to need know what the queries are.

Note: if the server has just started, first requests are slower. Java starts optimizing code and the file cache becomes more active over time.

author_association: MEMBER
user: login: Aklakan
created_at: 2025-03-04T20:04:51Z
body: I quite recently fixed several cases where timeouts were not working properly. Without your query load its hard to tell whether this is related to something I did not fix - or perhaps to something that I accidentally broke.

Are update requests involved?
Do the timeouts work properly for your query load with jena 5.0.0?


author_association: CONTRIBUTOR
user: login: mpagni12
created_at: 2025-03-05T12:44:49Z
body: Here is the log: [fuseki.log.gz](https://github.com/user-attachments/files/19089167/fuseki.log.gz)

Storage is TDB with no inference. 
author_association: NONE
user: login: mpagni12
created_at: 2025-03-05T12:45:21Z
body: > I quite recently fixed several cases where timeouts were not working properly. Without your query load its hard to tell whether this is related to something I did not fix - or perhaps to something that I accidentally broke.
> 
> Are update requests involved? Do the timeouts work properly for your query load with jena 5.0.0?

I am using 5.3.0, should I downgrade my version for testing?
author_association: NONE
user: login: Aklakan
created_at: 2025-03-05T13:29:23Z
body: > I am using 5.3.0, should I downgrade my version for testing?

That would be very helpful in order to determine whether the issue existed for a while or was introduced recently.
author_association: CONTRIBUTOR
user: login: mpagni12
created_at: 2025-03-05T15:55:17Z
body: I  rebuild a database and rerun the same test using 5.0.0: all lengthy queries timeout after 10 s :-)

This strongly suggests  that the bug was introduce later.
author_association: NONE
user: login: afs
created_at: 2025-03-05T16:21:45Z
body: I looked for `timeout=` and matched queries to their response code: e.g.  `[34] 503 Service Unavailable (10.879 s)` or `[26] 200 OK (7.066 s)`

I didn't find any 200's over 10 seconds.
I didn't find any 503's under 10 seconds.

I did find one outlier `[20] 503 Service Unavailable (377.566 s)`. It is like the query from #3021 without the suggested workaround but has extra work added.  The `OPTIONAL` look like they have a partial cross-product in them.

The timeout is tested every so often - it isn't an interrupt - so the query is in a busy CPU loop (probably fixable - caveat polling the timeout too much is a bit expensive).

author_association: MEMBER
user: login: Aklakan
created_at: 2025-03-05T19:07:37Z
body: So I can reproduce timeouts being ignored on property paths, such as this one on a dbpedia dataset (lots of labels):

```sparql
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?x { ?x rdfs:label/rdfs:label ?z }
```

```bash
time curl http://localhost:3030/ds --data-urlencode 'query=PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> SELECT ?x { ?x rdfs:label/rdfs:label ?z }' --data-urlencode 'timeout=1'
```

I have not analyzed property paths further.


I can also reproduce a similar (perhaps the same) problem with this query:
```sparql
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?x {
  ?x rdfs:label ?o
    { ?x rdfs:label ?z }
  UNION
    { BIND("hi" AS ?s) }
}
```

The problem is synchronization in conjunction with an eagerly executing iterator:

The timeout fires and tries to abort, but doesn't get the lock:

https://github.com/apache/jena/blob/6a86b5d1869c575142391e3e8bcc8f00f1a75132/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDataset.java#L364-L367

The lock is acquired here:

https://github.com/apache/jena/blob/6a86b5d1869c575142391e3e8bcc8f00f1a75132/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDataset.java#L438-L445


And while the lock is held `QueryExecDataset.startQueryIteratorActual` executes a hash join eagerly in the line `getPlan().iterator();` - so the abort signal due to timeout does not get through:

https://github.com/apache/jena/blob/6a86b5d1869c575142391e3e8bcc8f00f1a75132/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDataset.java#L472-L475


author_association: CONTRIBUTOR
user: login: afs
created_at: 2025-03-05T20:41:24Z
body: A sequence path should be expanded to triple patterns.

```sparql
{ ?x rdfs:label / rdfs:label ?z }
```
```sparql
{
?x rdfs:label ??P1 .
??P1  rdfs:label ?z .
}
```
Looking at the log, the timeout isn't getting forgotten - it is delayed (presumably by being busy in some tight loop).

The execution plan for the `[20]` does not have a hash join in it. It's `conditional` and `sequence` but I think the OPTIONALs can massive fan-out and should be hash left joins.

author_association: MEMBER
user: login: Aklakan
created_at: 2025-03-05T21:00:34Z
body: Removing the lock around `QueryExecDataset.startQueryIterator()` makes the query with `UNION { BIND() }` work (the eager hash join then gets cancelled because the signal is no longer blocked).

For the sequence path query, query execution hangs trying to produce a result with this stack trace - it does not appear to be affected by the locking issue - the context has the cancel signal correctly set to true but the signal is not considered.

![Image](https://github.com/user-attachments/assets/1f372d3e-a96b-4c22-913b-d84937a4fd14)

So it should be two separate issues.
author_association: CONTRIBUTOR
user: login: Aklakan
created_at: 2025-03-05T21:27:44Z
body: The problem I see with eager iterators in the snippet below is, that if `getPlan().iterator()` fails such as due to concurrent abort, then there is no iterator that can be closed -> likely resource leak. Or at least warnings from QueryIteratorCheck about non-closed iterators in the execCxt. (Or would closing the plan prevent that? - I don't yet know what resources this would close.)

https://github.com/apache/jena/blob/6a86b5d1869c575142391e3e8bcc8f00f1a75132/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDataset.java#L472-L475

IMO the clean solution would be to change AbstractIterHashJoin to be lazy - i.e. move buildHashTable to the hasNext method. Then the lock would only be held shortly for the iterator construction, and then the timeout can kick in normally.


author_association: CONTRIBUTOR
user: login: osma
created_at: 2025-03-13T08:59:13Z
body: I've been investigating a similar problem - probably the same one - where Fuseki 5.3.0 seems to ignore the timeout on certain inefficient queries. In the worst case that I saw, a single query was allowed to run for hours, even with the timeout set to 1 second (1000 ms). If you run a public or semi-public SPARQL endpoint, such queries are almost bound to happen eventually, and it leads to a DoS-style situation especially if such long-running queries start piling up. Each of them will fully utilize a single CPU core.

I only noticed this issue now, since it wasn't created yet when I started my investigation. I'm adding some details and information about my case in the hope that it's helpful, even though the main issue seems already known and a potential fix is available in PR #3047. (I will test the fix as well and provide comments)

I'm using a TDB2 dataset `tdb2-yso` where the file [yso-skos.ttl](https://github.com/NatLibFi/Finto-data/blob/master/vocabularies/yso/yso-skos.ttl) has been loaded into the graph `http://www.yso.fi/onto/yso/`

Here is a relatively simple (but stupid, I know) query that triggers the problem:

```
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT *
WHERE {
  GRAPH <http://www.yso.fi/onto/yso/> {
    ?uri ?p ?o .
    OPTIONAL {
      ?x skos:member ?o .
      FILTER NOT EXISTS {
        ?x skos:member ?other .
        FILTER NOT EXISTS {
          ?other skos:broader ?uri 
        }                       
      }                     
    }   
  }  VALUES (?uri) {
    (<http://www.yso.fi/onto/yso/p24009>) 
  } 
}
```

Here is my Fuseki configuration that sets a timeout of 2 seconds for the service (just to demonstrate that it won't help either) and 1 second for the dataset:

```
@prefix :      <http://base/#> .
@prefix tdb2:  <http://jena.apache.org/2016/tdb#> .
@prefix rdf:   <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix ja:    <http://jena.hpl.hp.com/2005/11/Assembler#> .
@prefix rdfs:  <http://www.w3.org/2000/01/rdf-schema#> .
@prefix fuseki: <http://jena.apache.org/fuseki#> .

[] rdf:type fuseki:Server ;
   ja:context [ ja:cxtName "arq:queryTimeout" ;  ja:cxtValue "2000" ] .

:tdb2  a                   fuseki:Service ;
    rdfs:label                          "TDB2" ;
    fuseki:dataset                      :dataset ;
    fuseki:name                         "ds" ;
    fuseki:serviceQuery                 "query" , "sparql" ;
    fuseki:serviceReadGraphStore        "get" ;
    fuseki:serviceReadWriteGraphStore   "data" ;
    fuseki:serviceUpdate                "update" ;
    fuseki:serviceUpload                "upload" .

:dataset
    a tdb2:DatasetTDB2 ;
    ja:context [ ja:cxtName "arq:queryTimeout" ;  ja:cxtValue "1000" ] ;
    tdb2:location  "tdb2-yso" .
```

Here is what happens with Fuseki 5.3.0 (and the current SNAPSHOT from `main`) when I run the query:

```
10:53:06 INFO  Server          :: Apache Jena Fuseki 5.3.0
10:53:06 INFO  Config          :: Fuseki Base = /home/oisuomin/proj/test-fuseki5-timeout/run
10:53:06 INFO  Config          :: Database: /ds
10:53:06 INFO  Config          :: UI Base = fuseki-server.jar
10:53:06 INFO  Server          :: Configuration file: fuseki-config.ttl
10:53:06 INFO  Server          :: Path = /ds
10:53:06 INFO  Server          ::   Memory: 4.0 GiB
10:53:06 INFO  Server          ::   Java:   21.0.6
10:53:06 INFO  Server          ::   OS:     Linux 6.11.0-19-generic amd64
10:53:06 INFO  Server          ::   PID:    480591
10:53:06 INFO  Shiro           :: Shiro configuration: file:/home/oisuomin/proj/test-fuseki5-timeout/run/shiro.ini
10:53:06 INFO  Server          :: Start Fuseki (http=3030)
10:53:11 INFO  Fuseki          :: [3] POST http://localhost:3030/ds/
10:53:11 INFO  Fuseki          :: [3] Query = PREFIX skos: <http://www.w3.org/2004/02/skos/core#> SELECT * WHERE {   GRAPH <http://www.yso.fi/onto/yso/> {     ?uri ?p ?o .     OPTIONAL {       ?x skos:member ?o .       FILTER NOT EXISTS {         ?x skos:member ?other .         FILTER NOT EXISTS {           ?other skos:broader ?uri          }                              }                          }      }  VALUES (?uri) {     (<http://www.yso.fi/onto/yso/p24009>)    }  }
10:53:22 INFO  Fuseki          :: [3] 503 Service Unavailable (10.769 s)
```

So the query runs for 10+ seconds before being cancelled. The 1 second timeout doesn't cut it off at 1 second as it should.

I tested earlier Jena Fuseki releases as well. 5.2.0, 5.1.0 and 5.0.0 all had the same problem. The last release where the timeout setting still worked was 4.10.0:

```
10:52:00 INFO  Server          :: Apache Jena Fuseki 4.10.0
10:52:00 INFO  Config          :: FUSEKI_HOME=/home/oisuomin/sw/apache-jena-fuseki-4.10.0
10:52:00 INFO  Config          :: FUSEKI_BASE=/home/oisuomin/proj/test-fuseki5-timeout/run
10:52:00 INFO  Config          :: Shiro file: file:///home/oisuomin/proj/test-fuseki5-timeout/run/shiro.ini
10:52:01 INFO  Server          :: Path = /ds
10:52:01 INFO  Server          ::   Memory: 4.0 GiB
10:52:01 INFO  Server          ::   Java:   21.0.6
10:52:01 INFO  Server          ::   OS:     Linux 6.11.0-19-generic amd64
10:52:01 INFO  Server          ::   PID:    480212
10:52:01 INFO  Server          :: Started 2025/03/13 10:52:01 EET on port 3030
10:52:03 INFO  Fuseki          :: [1] POST http://localhost:3030/ds/
10:52:03 INFO  Fuseki          :: [1] Query = PREFIX skos: <http://www.w3.org/2004/02/skos/core#> SELECT * WHERE {   GRAPH <http://www.yso.fi/onto/yso/> {     ?uri ?p ?o .     OPTIONAL {       ?x skos:member ?o .       FILTER NOT EXISTS {         ?x skos:member ?other .         FILTER NOT EXISTS {           ?other skos:broader ?uri          }                              }                          }      }  VALUES (?uri) {     (<http://www.yso.fi/onto/yso/p24009>)    }  }
10:52:04 INFO  Fuseki          :: [1] 503 Service Unavailable (1.020 s)
```

I'll provide comments on the PR separately in a moment (it seems to work).
author_association: CONTRIBUTOR

Diff:
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/iterator/QueryIterFilterExpr.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/iterator/QueryIterFilterExpr.java
index 893aa0f367d..1b2aa55f229 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/iterator/QueryIterFilterExpr.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/iterator/QueryIterFilterExpr.java
@@ -21,6 +21,7 @@
 import org.apache.jena.atlas.io.IndentedWriter;
 import org.apache.jena.atlas.lib.Lib;
 import org.apache.jena.atlas.logging.Log;
+import org.apache.jena.query.QueryCancelledException;
 import org.apache.jena.sparql.engine.ExecutionContext;
 import org.apache.jena.sparql.engine.QueryIterator;
 import org.apache.jena.sparql.engine.binding.Binding;
@@ -48,6 +49,9 @@ public Binding accept(Binding binding) {
             if ( expr.isSatisfied(binding, super.getExecContext()) )
                 return binding;
             return null;
+        } catch (QueryCancelledException ex) {
+            ex.addSuppressed(new RuntimeException("Query cancelled exception."));
+            throw ex;
         } catch (ExprException ex) {
             // Some evaluation exception: should not happen.
             Log.warn(this, "Expression Exception in " + expr, ex);
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/join/AbstractIterHashJoin.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/join/AbstractIterHashJoin.java
index 639b7bb0153..126be65a85c 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/join/AbstractIterHashJoin.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/join/AbstractIterHashJoin.java
@@ -43,8 +43,8 @@ public abstract class AbstractIterHashJoin extends QueryIter2 {
     protected long s_trailerResults       = 0 ;       // Results from the trailer iterator.
     // See also stats in the probe table.
 
-    protected final JoinKey               joinKey ;
-    protected final MultiHashProbeTable   hashTable ;
+    protected JoinKey                   joinKey ;
+    protected MultiHashProbeTable       hashTable ;
 
     private QueryIterator               iterStream ;
     private Binding                     rowStream       = null ;
@@ -61,40 +61,46 @@ enum Phase { INIT, HASH , STREAM, TRAILER, DONE }
     protected AbstractIterHashJoin(JoinKey initialJoinKey, QueryIterator probeIter, QueryIterator streamIter, ExecutionContext execCxt) {
         super(probeIter, streamIter, execCxt) ;
 
-        if ( initialJoinKey == null ) {
-            // This block computes an initial join key from the common variables of each iterator's first binding.
-            QueryIterPeek pProbe = QueryIterPeek.create(probeIter, execCxt) ;
-            QueryIterPeek pStream = QueryIterPeek.create(streamIter, execCxt) ;
+        this.joinKey = initialJoinKey ;
+        this.iterStream = streamIter ;
+        this.iterCurrent = null ;
+    }
 
-            Binding bLeft = pProbe.peek() ;
-            Binding bRight = pStream.peek() ;
+    private void doInit() {
+        QueryIterator probeIter = getLeft();
+        try {
+            if ( joinKey == null ) {
+                // This block computes an initial join key from the common variables of each iterator's first binding.
+                ExecutionContext execCxt = getExecContext();
+                QueryIterPeek pProbe = QueryIterPeek.create(probeIter, execCxt) ;
+                probeIter = pProbe ;
 
-            List<Var> varsLeft = Iter.toList(bLeft.vars()) ;
-            List<Var> varsRight = Iter.toList(bRight.vars()) ;
+                QueryIterPeek pStream = QueryIterPeek.create(iterStream, execCxt) ;
+                this.iterStream = pStream ;
 
-            initialJoinKey = JoinKey.create(varsLeft, varsRight) ;
+                Binding bLeft = pProbe.peek() ;
+                Binding bRight = pStream.peek() ;
 
-            probeIter = pProbe ;
-            streamIter = pStream ;
-        }
+                List<Var> varsLeft = Iter.toList(bLeft.vars()) ;
+                List<Var> varsRight = Iter.toList(bRight.vars()) ;
 
-        JoinKey maxJoinKey = null;
+                joinKey = JoinKey.create(varsLeft, varsRight) ;
+            }
 
-        this.joinKey = initialJoinKey ;
-        this.iterStream = streamIter ;
-        this.hashTable = new MultiHashProbeTable(maxJoinKey, initialJoinKey) ;
-        this.iterCurrent = null ;
-        buildHashTable(probeIter) ;
+            JoinKey maxJoinKey = null;
+            this.hashTable = new MultiHashProbeTable(maxJoinKey, joinKey) ;
+            buildHashTable(probeIter) ;
+        } finally {
+            probeIter.close();
+        }
     }
 
     private void buildHashTable(QueryIterator iter1) {
         state = Phase.HASH ;
-        for (; iter1.hasNext();) {
-            Binding row1 = iter1.next() ;
+        iter1.forEachRemaining(row1 -> {
             s_countProbe ++ ;
             hashTable.put(row1) ;
-        }
-        iter1.close() ;
+        });
         state = Phase.STREAM ;
     }
 
@@ -127,8 +133,10 @@ protected Binding moveToNextBindingOrNull() {
         switch ( state ) {
             case DONE : return null ;
             case HASH :
-            case INIT :
                 throw new IllegalStateException() ;
+            case INIT :
+                doInit();
+                break;
             case TRAILER :
                 return doOneTail() ;
             case STREAM :
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/solver/StageMatchTriple.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/solver/StageMatchTriple.java
index 58e59fab66b..f46ca62c0bc 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/solver/StageMatchTriple.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/main/solver/StageMatchTriple.java
@@ -20,12 +20,14 @@
 
 import java.util.Iterator;
 import java.util.Objects;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Predicate;
 
 import org.apache.jena.atlas.iterator.Iter;
 import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Triple;
+import org.apache.jena.query.QueryCancelledException;
 import org.apache.jena.sparql.core.Var;
 import org.apache.jena.sparql.engine.ExecutionContext;
 import org.apache.jena.sparql.engine.binding.Binding;
@@ -61,6 +63,18 @@ private static Iterator<Binding> accessTriple(Binding binding, Graph graph, Trip
         // ExtendedIterator<Triple> graphIter = graph.find(s2, p2, o2) ;
         // Language tags.
         ExtendedIterator<Triple> graphIter = G.findByLang(graph, s2, p2, o2);
+
+        // Add cancel.
+        AtomicBoolean cancelSignal = execCxt.getCancelSignal();
+        if (cancelSignal != null) {
+            graphIter = graphIter.mapWith(x -> {
+                if (cancelSignal.get()) {
+                    throw new QueryCancelledException();
+                }
+                return x;
+            });
+        }
+
         ExtendedIterator<Binding> iter = graphIter.mapWith( r -> mapper(resultsBuilder, s, p, o, r)).filterDrop(Objects::isNull);
         return iter;
     }
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetOps.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetOps.java
index 8a4dad07657..06eb4c11c93 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetOps.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetOps.java
@@ -30,9 +30,9 @@
 import org.apache.jena.sparql.core.Prologue;
 import org.apache.jena.sparql.resultset.ResultsWriter;
 
-/** 
+/**
  * RowSetOps - Convenience ways to call the various output formatters.
- * 
+ *
  * @see ResultSetMgr
  */
 
@@ -51,7 +51,7 @@ public static void consume(RowSet rowSet)
      * This operation consumes the RowSet.
      */
     public static long count(RowSet rowSet)
-    { return rowSet.rewindable().size(); }
+    { return rowSet.stream().count(); }
 
     /**
      * Output a result set in a text format.  The result set is consumed.
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetStream.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetStream.java
index f4d2d7d6e42..f816de00ca6 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetStream.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetStream.java
@@ -21,6 +21,7 @@
 import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
+import java.util.function.Consumer;
 
 import org.apache.jena.atlas.iterator.Iter;
 import org.apache.jena.sparql.core.Var;
@@ -72,4 +73,12 @@ public long getRowNumber() {
     public void close() {
         Iter.close(iterator);
     }
+
+    @Override
+    public void forEachRemaining(Consumer<? super Binding> action) {
+        iterator.forEachRemaining(b -> {
+            ++rowNumber;
+            action.accept(b);
+        });
+    }
 }
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/api/TestQueryExecutionTimeout2.java b/jena-arq/src/test/java/org/apache/jena/sparql/api/TestQueryExecutionTimeout2.java
index 7744ce2840e..70b791d1f89 100644
--- a/jena-arq/src/test/java/org/apache/jena/sparql/api/TestQueryExecutionTimeout2.java
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/api/TestQueryExecutionTimeout2.java
@@ -22,13 +22,30 @@
 import static org.junit.Assume.assumeFalse;
 
 import java.util.concurrent.TimeUnit;
+import java.util.stream.LongStream;
 
 import org.apache.jena.base.Sys ;
 import org.apache.jena.graph.Graph ;
-import org.apache.jena.query.* ;
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.graph.Triple;
+import org.apache.jena.graph.impl.GraphBase;
+import org.apache.jena.query.Dataset;
+import org.apache.jena.query.DatasetFactory;
+import org.apache.jena.query.QueryCancelledException;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.query.ResultSet;
+import org.apache.jena.query.ResultSetFormatter;
 import org.apache.jena.sparql.core.DatasetGraph ;
 import org.apache.jena.sparql.core.DatasetGraphFactory ;
 import org.apache.jena.sparql.engine.binding.Binding ;
+import org.apache.jena.sparql.engine.join.AbstractIterHashJoin;
+import org.apache.jena.sparql.engine.main.solver.StageMatchTriple;
+import org.apache.jena.sparql.exec.QueryExec;
+import org.apache.jena.sparql.exec.QueryExecDataset;
+import org.apache.jena.sparql.exec.RowSetOps;
+import org.apache.jena.util.iterator.ExtendedIterator;
+import org.apache.jena.util.iterator.NiceIterator;
+import org.apache.jena.util.iterator.WrappedIterator;
 import org.junit.Assert ;
 import org.junit.Before;
 import org.junit.Test ;
@@ -110,5 +127,83 @@ private static void exceptionExpected(ResultSet rs) {
     private int timeout(int time1, int time2) {
         return mayBeErratic ? time2 : time1 ;
     }
+
+    /**
+     * Test case for GH-3044.
+     * {@link AbstractIterHashJoin} used to eagerly populate a hash probe table on iterator construction,
+     * while {@link QueryExecDataset} held a lock that prevented async abort while the iterator was being constructed.
+     */
+    @Test(timeout = 5000, expected = QueryCancelledException.class)
+    public void test_timeout_hashJoin() {
+        // A very large virtual graph.
+        Graph graph = new GraphBase() {
+            @Override
+            protected ExtendedIterator<Triple> graphBaseFind(Triple triplePattern) {
+                return WrappedIterator.createNoRemove(LongStream.range(0, Long.MAX_VALUE)
+                    .mapToObj(i -> NodeFactory.createURI("http://www.example.org/r" + i))
+                    .peek(x -> {
+                        // Throttle binding generation to prevent going out-ouf-memory.
+                        // Bindings are likely to be stored in an in-memory hash probe table.
+                        try {
+                            Thread.sleep(1);
+                        } catch (InterruptedException e) {
+                            throw new RuntimeException(e);
+                        }
+                    })
+                    .map(i -> Triple.create(i, i, i))
+                    .filter(triplePattern::matches)
+                    .iterator());
+            }
+        };
+
+        try (QueryExec qe = QueryExec
+            .graph(graph)
+            .timeout(100, TimeUnit.MILLISECONDS)
+            .query("""
+            SELECT * {
+                ?a ?b ?c .
+                  # When this test case was written, a hash probe table was created for the rhs.
+                  {
+                    ?c ?d ?e .
+                  }
+                UNION
+                  { BIND('x' AS ?x) }
+            }
+            """).build()) {
+            RowSetOps.count(qe.select());
+        }
+    }
+
+    /** Test to ensure timeouts are considered in {@link StageMatchTriple} when producing only empty joins from a large set of triples. */
+    @Test(expected = QueryCancelledException.class)
+    public void test_timeout_stageMatchTriple() {
+        // A very large virtual graph that never matches a concrete subject.
+        Graph graph = new GraphBase() {
+            @Override
+            protected ExtendedIterator<Triple> graphBaseFind(Triple t) {
+                if (t.getMatchSubject() != null && t.getMatchSubject().isConcrete()) {
+                    // Don't match any concrete subject
+                    return NiceIterator.emptyIterator();
+                } else {
+                    // Generate :sI :p :oI triples
+                    return WrappedIterator.createNoRemove(LongStream.range(0, Long.MAX_VALUE)
+                        .mapToObj(i -> Triple.create(NodeFactory.createURI("http://www.example.org/s" + i), t.getPredicate(), NodeFactory.createURI("http://www.example.org/o" + i)))
+                        .iterator());
+                }
+            }
+        };
+
+        try (QueryExec qe = QueryExec
+            .graph(graph)
+            .timeout(100, TimeUnit.MILLISECONDS)
+            .query("""
+            SELECT * {
+                ?a <urn:p> ?c .
+                ?c <urn:p> ?e .
+            }
+            """).build()) {
+            RowSetOps.count(qe.select());
+        }
+    }
 }
 
diff --git a/jena-tdb1/src/main/java/org/apache/jena/tdb1/solver/StageMatchTuple.java b/jena-tdb1/src/main/java/org/apache/jena/tdb1/solver/StageMatchTuple.java
index 299187610fc..2ab35b13f5c 100644
--- a/jena-tdb1/src/main/java/org/apache/jena/tdb1/solver/StageMatchTuple.java
+++ b/jena-tdb1/src/main/java/org/apache/jena/tdb1/solver/StageMatchTuple.java
@@ -20,6 +20,7 @@
 
 import java.util.Iterator;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
@@ -28,6 +29,7 @@
 import org.apache.jena.atlas.lib.tuple.Tuple;
 import org.apache.jena.atlas.lib.tuple.TupleFactory;
 import org.apache.jena.graph.Node;
+import org.apache.jena.query.QueryCancelledException;
 import org.apache.jena.sparql.core.Var;
 import org.apache.jena.sparql.engine.ExecutionContext;
 import org.apache.jena.tdb1.store.NodeId;
@@ -68,6 +70,17 @@ private static Iterator<BindingNodeId> access(NodeTupleTable nodeTupleTable, Bin
             iterMatches = x.iterator();
         }
 
+        // Add cancel check.
+        AtomicBoolean cancelSignal = execCxt.getCancelSignal();
+        if (cancelSignal != null) {
+            iterMatches = Iter.map(iterMatches, x -> {
+                 if (cancelSignal.get()) {
+                     throw new QueryCancelledException();
+                 }
+                 return x;
+            });
+        }
+
         // ** Allow a triple or quad filter here.
         if ( filter != null )
             iterMatches = Iter.filter(iterMatches, filter);
diff --git a/jena-tdb2/src/main/java/org/apache/jena/tdb2/solver/StageMatchTuple.java b/jena-tdb2/src/main/java/org/apache/jena/tdb2/solver/StageMatchTuple.java
index 0be831369c5..3318b18390c 100644
--- a/jena-tdb2/src/main/java/org/apache/jena/tdb2/solver/StageMatchTuple.java
+++ b/jena-tdb2/src/main/java/org/apache/jena/tdb2/solver/StageMatchTuple.java
@@ -20,6 +20,7 @@
 
 import java.util.Iterator;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
@@ -28,6 +29,7 @@
 import org.apache.jena.atlas.lib.tuple.Tuple;
 import org.apache.jena.atlas.lib.tuple.TupleFactory;
 import org.apache.jena.graph.Node;
+import org.apache.jena.query.QueryCancelledException;
 import org.apache.jena.sparql.core.Var;
 import org.apache.jena.sparql.engine.ExecutionContext;
 import org.apache.jena.tdb2.store.NodeId;
@@ -68,6 +70,17 @@ private static Iterator<BindingNodeId> access(NodeTupleTable nodeTupleTable, Bin
             iterMatches = x.iterator();
         }
 
+        // Add cancel check.
+        AtomicBoolean cancelSignal = execCxt.getCancelSignal();
+        if (cancelSignal != null) {
+            iterMatches = Iter.map(iterMatches, x -> {
+                 if (cancelSignal.get()) {
+                     throw new QueryCancelledException();
+                 }
+                 return x;
+            });
+        }
+
         // ** Allow a triple or quad filter here.
         if ( filter != null )
             iterMatches = Iter.filter(iterMatches, filter);


Reviews:
user: login: Aklakan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T08:56:25Z
user: login: rvesse
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T09:10:04Z
user: login: Aklakan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:20:59Z
user: login: Aklakan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T09:57:19Z
user: login: afs
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T10:10:56Z
user: login: afs
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T10:15:38Z
user: login: Aklakan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-06T11:02:28Z
user: login: Aklakan
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T12:39:04Z
user: login: afs
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-24T17:37:11Z

Review comments:
diff_hunk: @@ -51,7 +51,7 @@ public static void consume(RowSet rowSet)
      * This operation consumes the RowSet.
      */
     public static long count(RowSet rowSet)
-    { return rowSet.rewindable().size(); }
+    { long c[] = {0}; rowSet.forEach(b -> ++c[0]); return c[0]; }
path: jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetOps.java
user: login: Aklakan
body: Intent: Avoid memory copy.
created_at: 2025-03-06T08:56:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,7 +51,7 @@ public static void consume(RowSet rowSet)
      * This operation consumes the RowSet.
      */
     public static long count(RowSet rowSet)
-    { return rowSet.rewindable().size(); }
+    { long c[] = {0}; rowSet.forEach(b -> ++c[0]); return c[0]; }
path: jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetOps.java
user: login: rvesse
body: Or maybe `rowSet.stream().count()` would be cleaner and still avoid the memory copy (though it has conversion to `Stream` overhead)?
created_at: 2025-03-06T09:10:04Z
author_association: MEMBER
diff_hunk: @@ -51,7 +51,7 @@ public static void consume(RowSet rowSet)
      * This operation consumes the RowSet.
      */
     public static long count(RowSet rowSet)
-    { return rowSet.rewindable().size(); }
+    { long c[] = {0}; rowSet.forEach(b -> ++c[0]); return c[0]; }
path: jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetOps.java
user: login: Aklakan
body: good point! - changed to the cleaner stream version.
created_at: 2025-03-06T09:20:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -68,6 +70,17 @@ private static Iterator<BindingNodeId> access(NodeTupleTable nodeTupleTable, Bin
             iterMatches = x.iterator();
         }
 
+        // Add cancel check.
path: jena-tdb2/src/main/java/org/apache/jena/tdb2/solver/StageMatchTuple.java
user: login: Aklakan
body: As a separate case where timeouts get ignored (besides the hash probe table construction), a cancel check is needed here for the following query on TDB2:

```sparql
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?x { ?x rdfs:label ?y . ?y rdfs:label ?z }
```

```
time curl http://localhost:3030/ds --data-urlencode 'query=PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> SELECT ?x { ?x rdfs:label ?y . ?y rdfs:label ?z }' --data-urlencode 'timeout=1'
```

Against this preloaded [TDB2 DBpedia dataset](https://maven.aksw.org/archiva/#artifact-details-download-content~internal/tdb2.org.aksw.data.text2sparql.2025/dbpedia/1.0.0) (~30GB)

created_at: 2025-03-06T09:57:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -51,7 +51,7 @@ public static void consume(RowSet rowSet)
      * This operation consumes the RowSet.
      */
     public static long count(RowSet rowSet)
-    { return rowSet.rewindable().size(); }
+    { long c[] = {0}; rowSet.forEach(b -> ++c[0]); return c[0]; }
path: jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetOps.java
user: login: afs
body: Maybe it does not matter with this destructive operation but the cost of `stream()` is not trivial. 
`forEach` does not use `stream`.



created_at: 2025-03-06T10:10:56Z
author_association: MEMBER
diff_hunk: @@ -51,7 +51,7 @@ public static void consume(RowSet rowSet)
      * This operation consumes the RowSet.
      */
     public static long count(RowSet rowSet)
-    { return rowSet.rewindable().size(); }
+    { long c[] = {0}; rowSet.forEach(b -> ++c[0]); return c[0]; }
path: jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetOps.java
user: login: afs
body: > Intent: Avoid memory copy.

`RowSetMem` rewind should not be a memory copy. It shares the rows array.

A rewindable RowSet (e.g. Mem) goes back to the beginning where as `forEach` continues the iteration.

created_at: 2025-03-06T10:15:38Z
author_association: MEMBER
diff_hunk: @@ -51,7 +51,7 @@ public static void consume(RowSet rowSet)
      * This operation consumes the RowSet.
      */
     public static long count(RowSet rowSet)
-    { return rowSet.rewindable().size(); }
+    { long c[] = {0}; rowSet.forEach(b -> ++c[0]); return c[0]; }
path: jena-arq/src/main/java/org/apache/jena/sparql/exec/RowSetOps.java
user: login: Aklakan
body: Behavior so far (IIRC):
* For non-RowSetMem instances counting would create an intermediate in-memory copy.
* The count for a RowSetMem instances would be the total number of bindings in the in-memory row set - rather than the number of remaining bindings.

I added the method `RowSetStream.forEachRemaining` - the stack trace for `RowSetOps.count` using `rowSet.stream().count()` then becomes a delegate to the forEachRemaining method (with some stream creation overhead that should be neglectable for larger counts):

![image](https://github.com/user-attachments/assets/a9726dfa-4a7b-4dbc-b243-47ef2a18db43)

created_at: 2025-03-06T11:02:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -48,6 +49,9 @@ public Binding accept(Binding binding) {
             if ( expr.isSatisfied(binding, super.getExecContext()) )
                 return binding;
             return null;
+        } catch (QueryCancelledException ex) {
+            ex.addSuppressed(new RuntimeException("Query cancelled exception."));
+            throw ex;
path: jena-arq/src/main/java/org/apache/jena/sparql/engine/iterator/QueryIterFilterExpr.java
user: login: Aklakan
body: Should mitigate the case of ugly logging reported by @osma .
created_at: 2025-03-13T12:39:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: Aklakan
created_at: 2025-03-06T10:28:34Z
body: Set to draft because I am still investigating this query on in memory datasets.

```sparql
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?x { ?x rdfs:label ?y . ?y rdfs:label ?z }
```
author_association: CONTRIBUTOR
user: login: Aklakan
created_at: 2025-03-06T11:08:07Z
body: So for the "empty join" case I had to add cancel signal checks to the `StageMatchTriple` (added a test case) and `StageMatchTuple` (so far only tested locally against a Fuseki). On a larger scale the issue is, that the iterator of `graph.find()` would actually already need cancellation support. For example, a custom `graph.find()` implementation could sequentially search a large file for matching triples - and if there are no matches in that file, then a call to `hasNext` would scan all data without any means to interrupt the process. Right?
author_association: CONTRIBUTOR
user: login: Aklakan
created_at: 2025-03-06T16:30:18Z
body: To summarize:

* Aborting the now lazy HashJoin has a dedicated test.
* For the empty join case, there is a dedicated test for `StageMatchTriple` based on a custom `GraphBase.find()` implementation that returns an infinite stream of matches.
* But: not sure how I can mock a similar test for `StageMatchTuple` in TDB1/2, because it requires some large dataset or mocking NodeIds. Here is a brief screen-capture of the effect of this PR's code:

[CancelStageMatchTuple.webm](https://github.com/user-attachments/assets/8ef59741-6db3-472f-a392-cac811393423)


author_association: CONTRIBUTOR
user: login: osma
created_at: 2025-03-13T09:03:48Z
body: I had the same problem, as mentioned in https://github.com/apache/jena/issues/3044#issuecomment-2720484943.

I tested the code from the PR branch and it seems to solve the problem:

```
11:00:39 INFO  Server          :: Apache Jena Fuseki 5.4.0-SNAPSHOT
11:00:39 INFO  Config          :: Fuseki Base = /home/oisuomin/proj/test-fuseki5-timeout/run
11:00:39 INFO  Config          :: Database: /ds
11:00:39 INFO  Config          :: UI Base = fuseki-server.jar
11:00:39 INFO  Server          :: Configuration file: fuseki-config.ttl
11:00:39 INFO  Server          :: Path = /ds
11:00:39 INFO  Server          ::   Memory: 4.0 GiB
11:00:39 INFO  Server          ::   Java:   21.0.6
11:00:39 INFO  Server          ::   OS:     Linux 6.11.0-19-generic amd64
11:00:39 INFO  Server          ::   PID:    483024
11:00:39 INFO  Shiro           :: Shiro configuration: file:/home/oisuomin/proj/test-fuseki5-timeout/run/shiro.ini
11:00:39 INFO  Server          :: Start Fuseki (http=3030)
11:00:43 INFO  Fuseki          :: [3] POST http://localhost:3030/ds/
11:00:43 INFO  Fuseki          :: [3] Query = PREFIX skos: <http://www.w3.org/2004/02/skos/core#> SELECT * WHERE {   GRAPH <http://www.yso.fi/onto/yso/> {     ?uri ?p ?o .     OPTIONAL {       ?x skos:member ?o .       FILTER NOT EXISTS {         ?x skos:member ?other .         FILTER NOT EXISTS {           ?other skos:broader ?uri          }                              }                          }      }  VALUES (?uri) {     (<http://www.yso.fi/onto/yso/p24009>)    }  }
11:00:44 INFO  Fuseki          :: [3] 503 Service Unavailable (1.021 s)
```

One thing I noticed that occasionally, some pretty long tracebacks end up in the log when the timeout triggers. However, this happened once or twice with Fuseki 4.10.0 as well, so it's not a new thing. It just looks ugly in the log:

```
11:01:15 INFO  Fuseki          :: [4] POST http://localhost:3030/ds/
11:01:15 INFO  Fuseki          :: [4] Query = PREFIX skos: <http://www.w3.org/2004/02/skos/core#> SELECT * WHERE {   GRAPH <http://www.yso.fi/onto/yso/> {     ?uri ?p ?o .     OPTIONAL {       ?x skos:member ?o .       FILTER NOT EXISTS {         ?x skos:member ?other .         FILTER NOT EXISTS {           ?other skos:broader ?uri          }                              }                          }      }  VALUES (?uri) {     (<http://www.yso.fi/onto/yso/p24009>)    }  }
11:01:16 WARN  QueryIterFilterExpr :: General exception in (notexists
   (filter (notexists
              (quadpattern (quad <http://www.yso.fi/onto/yso/> ?other <http://www.w3.org/2004/02/skos/core#broader> <http://www.yso.fi/onto/yso/p26543>)))
     (quadpattern (quad <http://www.yso.fi/onto/yso/> ?x <http://www.w3.org/2004/02/skos/core#member> ?other))))
org.apache.jena.query.QueryCancelledException: null
	at org.apache.jena.tdb2.solver.StageMatchTuple.lambda$access$1(StageMatchTuple.java:78) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.atlas.iterator.Iter$IterMap.next(Iter.java:439) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.atlas.iterator.Iter$IterMap.next(Iter.java:439) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.atlas.iterator.Iter.next(Iter.java:1161) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.atlas.iterator.Iter$IterFiltered.hasNext(Iter.java:282) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.atlas.iterator.Iter.hasNext(Iter.java:1156) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.atlas.iterator.IteratorFlatMap.hasNext(IteratorFlatMap.java:59) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.IterAbortable.hasNext(IterAbortable.java:59) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.atlas.iterator.Iter$IterMap.hasNext(Iter.java:434) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIterPlainWrapper.hasNextBinding(QueryIterPlainWrapper.java:59) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIteratorBase.hasNext(QueryIteratorBase.java:122) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIterProcessBinding.hasNextBinding(QueryIterProcessBinding.java:76) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIteratorBase.hasNext(QueryIteratorBase.java:122) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIteratorWrapper.hasNextBinding(QueryIteratorWrapper.java:37) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIteratorBase.hasNext(QueryIteratorBase.java:122) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.expr.E_NotExists.eval(E_NotExists.java:69) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.expr.ExprFunctionOp.eval(ExprFunctionOp.java:72) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.expr.ExprNode.isSatisfied(ExprNode.java:42) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIterFilterExpr.accept(QueryIterFilterExpr.java:48) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIterProcessBinding.hasNextBinding(QueryIterProcessBinding.java:80) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIteratorBase.hasNext(QueryIteratorBase.java:122) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIterDefaulting.hasNextBinding(QueryIterDefaulting.java:54) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIteratorBase.hasNext(QueryIteratorBase.java:122) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIterRepeatApply.hasNextBinding(QueryIterRepeatApply.java:69) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIteratorBase.hasNext(QueryIteratorBase.java:122) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIterPeek.hasNextBinding(QueryIterPeek.java:57) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIteratorBase.hasNext(QueryIteratorBase.java:122) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:132) ~[?:?]
	at org.apache.jena.sparql.engine.join.AbstractIterHashJoin.buildHashTable(AbstractIterHashJoin.java:100) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.join.AbstractIterHashJoin.doInit(AbstractIterHashJoin.java:92) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.join.AbstractIterHashJoin.moveToNextBindingOrNull(AbstractIterHashJoin.java:138) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.join.AbstractIterHashJoin.hasNextBinding(AbstractIterHashJoin.java:112) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIteratorBase.hasNext(QueryIteratorBase.java:122) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIteratorWrapper.hasNextBinding(QueryIteratorWrapper.java:37) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIteratorBase.hasNext(QueryIteratorBase.java:122) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIteratorWrapper.hasNextBinding(QueryIteratorWrapper.java:37) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.engine.iterator.QueryIteratorBase.hasNext(QueryIteratorBase.java:122) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.sparql.exec.RowSetStream.hasNext(RowSetStream.java:48) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.servlets.SPARQLQueryProcessor.executeQuery(SPARQLQueryProcessor.java:391) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.servlets.SPARQLQueryProcessor.execute(SPARQLQueryProcessor.java:284) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.servlets.SPARQLQueryProcessor.executeWithParameter(SPARQLQueryProcessor.java:224) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.servlets.SPARQLQueryProcessor.execute(SPARQLQueryProcessor.java:209) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.servlets.ActionService.executeLifecycle(ActionService.java:58) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.servlets.SPARQLQueryProcessor.execPost(SPARQLQueryProcessor.java:84) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.servlets.ActionProcessor.process(ActionProcessor.java:34) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.servlets.ActionBase.process(ActionBase.java:54) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.servlets.ActionExecLib.execActionSub(ActionExecLib.java:124) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.servlets.ActionExecLib.execAction(ActionExecLib.java:98) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.server.Dispatcher.dispatchAction(Dispatcher.java:240) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.server.Dispatcher.process(Dispatcher.java:230) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.server.Dispatcher.dispatch(Dispatcher.java:148) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.servlets.FusekiFilter.doFilter(FusekiFilter.java:49) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.ee10.servlet.FilterHolder.doFilter(FilterHolder.java:205) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.ee10.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1591) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:65) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:109) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:138) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:156) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:70) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.web.servlet.AdviceFilter.executeChain(AdviceFilter.java:109) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.web.servlet.AdviceFilter.doFilterInternal(AdviceFilter.java:138) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:156) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.web.servlet.ProxiedFilterChain.doFilter(ProxiedFilterChain.java:70) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.web.servlet.AbstractShiroFilter.executeChain(AbstractShiroFilter.java:463) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.web.servlet.AbstractShiroFilter.lambda$doFilterInternal$0(AbstractShiroFilter.java:378) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.subject.support.SubjectCallable.doCall(SubjectCallable.java:91) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.subject.support.SubjectCallable.call(SubjectCallable.java:84) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.subject.support.DelegatingSubject.execute(DelegatingSubject.java:389) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.web.servlet.AbstractShiroFilter.doFilterInternal(AbstractShiroFilter.java:376) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.shiro.web.servlet.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:156) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.ee10.servlet.FilterHolder.doFilter(FilterHolder.java:205) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.ee10.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1591) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.servlets.CrossOriginFilter.handle(CrossOriginFilter.java:351) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.apache.jena.fuseki.servlets.CrossOriginFilter.doFilter(CrossOriginFilter.java:304) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.ee10.servlet.FilterHolder.doFilter(FilterHolder.java:205) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.ee10.servlet.ServletHandler$Chain.doFilter(ServletHandler.java:1591) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.ee10.servlet.ServletHandler$MappedServlet.handle(ServletHandler.java:1552) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.ee10.servlet.ServletChannel.dispatch(ServletChannel.java:819) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.ee10.servlet.ServletChannel.handle(ServletChannel.java:436) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.ee10.servlet.ServletHandler.handle(ServletHandler.java:469) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.ee10.servlet.SessionHandler.handle(SessionHandler.java:717) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.server.handler.ContextHandler.handle(ContextHandler.java:1060) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.server.Server.handle(Server.java:182) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.server.internal.HttpChannelState$HandlerInvoker.run(HttpChannelState.java:662) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.server.internal.HttpConnection.onFillable(HttpConnection.java:418) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.io.AbstractConnection$ReadCallback.succeeded(AbstractConnection.java:322) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.io.FillInterest.fillable(FillInterest.java:99) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.io.SelectableChannelEndPoint$1.run(SelectableChannelEndPoint.java:53) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.runTask(AdaptiveExecutionStrategy.java:480) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.consumeTask(AdaptiveExecutionStrategy.java:443) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.tryProduce(AdaptiveExecutionStrategy.java:293) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.util.thread.strategy.AdaptiveExecutionStrategy.run(AdaptiveExecutionStrategy.java:201) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.util.thread.ReservedThreadExecutor$ReservedThread.run(ReservedThreadExecutor.java:311) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.util.thread.QueuedThreadPool.runJob(QueuedThreadPool.java:979) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.doRunJob(QueuedThreadPool.java:1209) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at org.eclipse.jetty.util.thread.QueuedThreadPool$Runner.run(QueuedThreadPool.java:1164) ~[fuseki-server.jar:5.4.0-SNAPSHOT]
	at java.base/java.lang.Thread.run(Thread.java:1583) [?:?]
11:01:16 INFO  Fuseki          :: [4] 503 Service Unavailable (1.015 s)
```

Great work @Aklakan , thanks for providing a fix!
author_association: CONTRIBUTOR
user: login: Aklakan
created_at: 2025-03-13T11:04:06Z
body: Hm, the long stack trace is not ideal - probably some additional exception catching is needed in the bridge between the `QueryIter(ator)` and plain iterator levels.

Update: The line `11:01:16 WARN  QueryIterFilterExpr :: General exception in (notexists[...]` suggests logging going on - the `QueryCancelledException` should be excluded there.
author_association: CONTRIBUTOR
user: login: osma
created_at: 2025-03-13T11:13:59Z
body: @Aklakan For these tests, I was running `fuseki-server --config fuseki-config.ttl` with no additional logging configuration.
author_association: CONTRIBUTOR