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

rustyrazorblade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/cassandra-easy-stress.git


The following commit(s) were added to refs/heads/main by this push:
     new 40e25eb  Fix pagination (#107)
40e25eb is described below

commit 40e25eb796308c6e0cafae4ea96170d05e670d08
Author: Jon Haddad <[email protected]>
AuthorDate: Tue Oct 13 19:32:38 2026 -0700

    Fix pagination (#107)
    
    * Fix --paginate looping forever on the first page
    
    In driver v4, AsyncResultSet.fetchNextPage() returns a new page and
    does not advance the current one. The callback called it in a loop on
    the first page, so hasMorePages() never became false. Each pass sent
    another request for page 2, and the driver I/O thread never returned.
    
    Chain the page fetches instead, and record the timer and the result
    after the last page arrives. A failed page fetch is now an error.
    
    * Move pagination out of OperationCallback
    
    Fetch the remaining pages where the request future is built, with an
    AsyncResultSet.fetchAllPages() extension, instead of inside the result
    callback. OperationCallback goes back to its single result/error path
    and loses the paginate flag; a failed page fetch reaches the existing
    error branch.
    
    The callback now gets the last page, so the first page is not held for
    the whole fetch. The DDL special case and the mutable paginate flag are
    gone: a single-page result passes straight through.
    
    
    Reviewed by Nate McCall
---
 .../cassandra/easystress/OperationCallback.kt      |  8 ---
 .../org/apache/cassandra/easystress/Pagination.kt  | 33 ++++++++++
 .../apache/cassandra/easystress/WorkloadRunner.kt  | 26 ++------
 .../apache/cassandra/easystress/PaginationTest.kt  | 77 ++++++++++++++++++++++
 4 files changed, 116 insertions(+), 28 deletions(-)

diff --git 
a/src/main/kotlin/org/apache/cassandra/easystress/OperationCallback.kt 
b/src/main/kotlin/org/apache/cassandra/easystress/OperationCallback.kt
index c61d5c2..b41eab0 100644
--- a/src/main/kotlin/org/apache/cassandra/easystress/OperationCallback.kt
+++ b/src/main/kotlin/org/apache/cassandra/easystress/OperationCallback.kt
@@ -36,7 +36,6 @@ class OperationCallback(
     val op: Operation,
     val startNanos: Long,
     val populatePhase: Boolean,
-    val paginate: Boolean = false,
 ) : BiConsumer<AsyncResultSet?, Throwable?> {
     companion object {
         val log = logger()
@@ -53,13 +52,6 @@ class OperationCallback(
             return
         }
 
-        // Handle pagination in driver v4
-        if (paginate && result != null) {
-            // Fetch next page - this could be made async but we'll keep it 
simple for now
-            while (result.hasMorePages()) {
-                result.fetchNextPage()
-            }
-        }
         val endNanos = System.nanoTime()
         context.timer(op, populatePhase).update(endNanos - op.createdAtNanos, 
TimeUnit.NANOSECONDS)
         // TODO (visibility): include details about paging?
diff --git a/src/main/kotlin/org/apache/cassandra/easystress/Pagination.kt 
b/src/main/kotlin/org/apache/cassandra/easystress/Pagination.kt
new file mode 100644
index 0000000..a4c7b36
--- /dev/null
+++ b/src/main/kotlin/org/apache/cassandra/easystress/Pagination.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.cassandra.easystress
+
+import com.datastax.oss.driver.api.core.cql.AsyncResultSet
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.CompletionStage
+
+/**
+ * Fetches every remaining page and completes with the last one.
+ * Each page is a separate AsyncResultSet; fetchNextPage() does not advance 
the receiver.
+ */
+fun AsyncResultSet.fetchAllPages(): CompletionStage<AsyncResultSet> =
+    if (hasMorePages()) {
+        fetchNextPage().thenCompose { it.fetchAllPages() }
+    } else {
+        CompletableFuture.completedFuture(this)
+    }
diff --git a/src/main/kotlin/org/apache/cassandra/easystress/WorkloadRunner.kt 
b/src/main/kotlin/org/apache/cassandra/easystress/WorkloadRunner.kt
index 0272403..5ead439 100644
--- a/src/main/kotlin/org/apache/cassandra/easystress/WorkloadRunner.kt
+++ b/src/main/kotlin/org/apache/cassandra/easystress/WorkloadRunner.kt
@@ -146,14 +146,13 @@ class WorkloadRunner(
 
         // pull requests off the queue instead of using generateKey
         // move the getNextOperation into the queue thing
-        var paginate = context.mainArguments.paginate
+        val paginate = context.mainArguments.paginate
         for (op in queue.getNextOperation()) {
             // In driver v4, async execution returns a CompletionStage
             val startNanos = System.nanoTime()
             val future =
                 when (op) {
                     is Operation.DDL -> {
-                        paginate = false
                         // Create a SimpleStatement for DDL operations
                         
context.session.executeAsync(SimpleStatement.newInstance(op.statement!!))
                     }
@@ -163,20 +162,10 @@ class WorkloadRunner(
                     }
                 }
 
-            // Create callback to handle the result
-            val callback =
-                OperationCallback(
-                    context,
-                    runner,
-                    op,
-                    startNanos,
-                    queue.populatePhase,
-                    paginate = paginate,
-                )
-
-            future.whenComplete { result, error ->
-                callback.accept(result, error)
-            }
+            // with pagination the operation completes after the last page 
arrives
+            val completed = if (paginate) future.thenCompose { 
it.fetchAllPages() } else future
+
+            completed.whenComplete(OperationCallback(context, runner, op, 
startNanos, queue.populatePhase))
         }
     }
 
@@ -220,12 +209,9 @@ class WorkloadRunner(
                         op,
                         startNanos,
                         queue.populatePhase,
-                        paginate = false,
                     )
 
-                future.whenComplete { result, error ->
-                    callback.accept(result, error)
-                }
+                future.whenComplete(callback)
             }
         } catch (_: OperationStopException) {
             log.info("Received Stop signal")
diff --git a/src/test/kotlin/org/apache/cassandra/easystress/PaginationTest.kt 
b/src/test/kotlin/org/apache/cassandra/easystress/PaginationTest.kt
new file mode 100644
index 0000000..15e5505
--- /dev/null
+++ b/src/test/kotlin/org/apache/cassandra/easystress/PaginationTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.cassandra.easystress
+
+import com.datastax.oss.driver.api.core.cql.AsyncResultSet
+import io.mockk.every
+import io.mockk.mockk
+import io.mockk.verify
+import org.assertj.core.api.Assertions.assertThat
+import org.assertj.core.api.Assertions.catchThrowable
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.Timeout
+import java.util.concurrent.CompletableFuture
+
+// a pagination hang must fail the test rather than block the build
+@Timeout(value = 5, threadMode = Timeout.ThreadMode.SEPARATE_THREAD)
+class PaginationTest {
+    private fun page(next: CompletableFuture<AsyncResultSet>?): AsyncResultSet 
=
+        mockk {
+            every { hasMorePages() } returns (next != null)
+            if (next != null) {
+                every { fetchNextPage() } returns next
+            }
+        }
+
+    @Test
+    fun `fetches each page once and completes with the last page`() {
+        val thirdFuture = CompletableFuture<AsyncResultSet>()
+        val third = page(null)
+        val second = page(thirdFuture)
+        val first = page(CompletableFuture.completedFuture(second))
+
+        val all = first.fetchAllPages().toCompletableFuture()
+
+        verify(exactly = 1) { first.fetchNextPage() }
+        verify(exactly = 1) { second.fetchNextPage() }
+        // the last page has not arrived yet
+        assertThat(all).isNotDone
+
+        thirdFuture.complete(third)
+
+        assertThat(all).isCompletedWithValue(third)
+    }
+
+    @Test
+    fun `completes a single page without fetching`() {
+        val only = page(null)
+
+        
assertThat(only.fetchAllPages().toCompletableFuture()).isCompletedWithValue(only)
+    }
+
+    @Test
+    fun `fails when a page fetch fails`() {
+        val failure = RuntimeException("page fetch failed")
+        val first = page(CompletableFuture.failedFuture(failure))
+
+        val all = first.fetchAllPages().toCompletableFuture()
+
+        assertThat(all).isCompletedExceptionally
+        assertThat(catchThrowable { all.join() }.cause).isSameAs(failure)
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to