All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.datastax.oss.driver.api.core.paging.OffsetPager Maven / Gradle / Ivy

The newest version!
/*
 * 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 com.datastax.oss.driver.api.core.paging;

import com.datastax.oss.driver.api.core.AsyncPagingIterable;
import com.datastax.oss.driver.api.core.MappedAsyncPagingIterable;
import com.datastax.oss.driver.api.core.PagingIterable;
import com.datastax.oss.driver.api.core.cql.ResultSet;
import com.datastax.oss.driver.api.core.cql.Statement;
import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableList;
import edu.umd.cs.findbugs.annotations.NonNull;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import net.jcip.annotations.ThreadSafe;

/**
 * A utility to emulate offset queries on the client side (this comes with important performance
 * trade-offs, make sure you read and understand the whole javadocs before using this class).
 *
 * 

Web UIs and services often provide paginated results with random access, for example: given a * page size of 20 elements, fetch page 5. Cassandra does not support this natively (see CASSANDRA-6511), because such * queries are inherently linear: the database would have to restart from the beginning every time, * and skip unwanted rows until it reaches the desired offset. * *

However, random pagination is a real need for many applications, and linear performance can be * a reasonable trade-off if the cardinality stays low. This class provides a way to emulate this * behavior on the client side. * *

Performance considerations

* * For each page that you want to retrieve: * *
    *
  • you need to re-execute the query, in order to start with a fresh result set; *
  • this class starts iterating from the beginning, and skips rows until it reaches the desired * offset. *
* *
* *
 * String query = "SELECT ...";
 * OffsetPager pager = new OffsetPager(20);
 *
 * // Get page 2: start from a fresh result set, throw away rows 1-20, then return rows 21-40
 * ResultSet rs = session.execute(query);
 * OffsetPager.Page<Row> page2 = pager.getPage(rs, 2);
 *
 * // Get page 5: start from a fresh result set, throw away rows 1-80, then return rows 81-100
 * rs = session.execute(query);
 * OffsetPager.Page<Row> page5 = pager.getPage(rs, 5);
 * 
* *

Establishing application-level guardrails

* * Linear performance should be fine for the values typically encountered in real-world * applications: for example, if the page size is 25 and users never go past page 10, the worst case * is only 250 rows, which is a very small result set. However, we strongly recommend that you * implement hard limits in your application code: if the page number is exposed to the user (for * example if it is passed as a URL parameter), make sure it is properly validated and enforce a * maximum, so that an attacker can't inject a large value that could potentially fetch millions of * rows. * *

Relation with protocol-level paging

* * Protocol-level paging refers to the ability to split large response into multiple network chunks: * see {@link Statement#setPageSize(int)} and {@code basic.request.page-size} in the configuration. * It happens under the hood, and is completely transparent for offset paging: this class will work * the same no matter how many network roundtrips were needed to fetch the result. You don't need to * set the protocol page size and the logical page size to the same value. */ @ThreadSafe public class OffsetPager { /** A page returned as the result of an offset query. */ public interface Page { /** The elements in the page. */ @NonNull List getElements(); /** * The page number (1 for the first page, 2 for the second page, etc). * *

Note that it may be different than the number you passed to {@link * #getPage(PagingIterable, int)}: if the result set was too short, this is the actual number of * the last page. */ int getPageNumber(); /** Whether this is the last page in the result set. */ boolean isLast(); } private final int pageSize; /** * Creates a new instance. * * @param pageSize the number of elements per page. Must be greater than or equal to 1. */ public OffsetPager(int pageSize) { if (pageSize < 1) { throw new IllegalArgumentException("Invalid pageSize, expected >=1, got " + pageSize); } this.pageSize = pageSize; } /** * Extracts a page from a synchronous result set, by skipping rows until we get to the requested * offset. * * @param iterable the iterable to extract the results from: typically a {@link ResultSet}, or a * {@link PagingIterable} returned by the mapper. * @param targetPageNumber the page to return (1 for the first page, 2 for the second page, etc). * Must be greater than or equal to 1. * @return the requested page, or the last page if the requested page was past the end of the * iterable. * @throws IllegalArgumentException if the conditions on the arguments are not respected. */ @NonNull public Page getPage( @NonNull PagingIterable iterable, final int targetPageNumber) { throwIfIllegalArguments(iterable, targetPageNumber); // Holds the contents of the target page. We also need to record the current page as we go, // because our iterable is forward-only and we can't predict when we'll hit the end. List currentPageElements = new ArrayList<>(); int currentPageNumber = 1; int currentPageSize = 0; for (ElementT element : iterable) { currentPageSize += 1; if (currentPageSize > pageSize) { currentPageNumber += 1; currentPageSize = 1; currentPageElements.clear(); } currentPageElements.add(element); if (currentPageNumber == targetPageNumber && currentPageSize == pageSize) { // The target page has the full size and we've seen all of its elements break; } } // Either we have the full target page, or we've reached the end of the result set. boolean isLast = iterable.one() == null; return new DefaultPage<>(currentPageElements, currentPageNumber, isLast); } /** * Extracts a page from an asynchronous result set, by skipping rows until we get to the requested * offset. * * @param iterable the iterable to extract the results from. Typically an {@link * AsyncPagingIterable}, or a {@link MappedAsyncPagingIterable} returned by the mapper. * @param targetPageNumber the page to return (1 for the first page, 2 for the second page, etc). * Must be greater than or equal to 1. * @return a stage that will complete with the requested page, or the last page if the requested * page was past the end of the iterable. * @throws IllegalArgumentException if the conditions on the arguments are not respected. */ @NonNull public > CompletionStage> getPage( @NonNull IterableT iterable, final int targetPageNumber) { // Throw IllegalArgumentException directly instead of failing the stage, since it signals // blatant programming errors throwIfIllegalArguments(iterable, targetPageNumber); CompletableFuture> pageFuture = new CompletableFuture<>(); getPage(iterable, targetPageNumber, 1, 0, new ArrayList<>(), pageFuture); return pageFuture; } private void throwIfIllegalArguments(@NonNull Object iterable, int targetPageNumber) { Objects.requireNonNull(iterable); if (targetPageNumber < 1) { throw new IllegalArgumentException( "Invalid targetPageNumber, expected >=1, got " + targetPageNumber); } } /** * Main method for the async iteration. * *

See the synchronous version in {@link #getPage(PagingIterable, int)} for more explanations: * this is identical, except that it is async and we need to handle protocol page transitions * manually. */ private , ElementT> void getPage( @NonNull IterableT iterable, final int targetPageNumber, int currentPageNumber, int currentPageSize, @NonNull List currentPageElements, @NonNull CompletableFuture> pageFuture) { // Note: iterable.currentPage()/fetchNextPage() refer to protocol-level pages, do not confuse // with logical pages handled by this class Iterator currentFrame = iterable.currentPage().iterator(); while (currentFrame.hasNext()) { ElementT element = currentFrame.next(); currentPageSize += 1; if (currentPageSize > pageSize) { currentPageNumber += 1; currentPageSize = 1; currentPageElements.clear(); } currentPageElements.add(element); if (currentPageNumber == targetPageNumber && currentPageSize == pageSize) { // Full-size target page. In this method it's simpler to finish directly here. if (currentFrame.hasNext()) { pageFuture.complete(new DefaultPage<>(currentPageElements, currentPageNumber, false)); } else if (!iterable.hasMorePages()) { pageFuture.complete(new DefaultPage<>(currentPageElements, currentPageNumber, true)); } else { // It's possible for the server to return an empty last frame, so we need to fetch it to // know for sure whether there are more elements int finalCurrentPageNumber = currentPageNumber; iterable .fetchNextPage() .whenComplete( (nextIterable, throwable) -> { if (throwable != null) { pageFuture.completeExceptionally(throwable); } else { boolean isLastPage = !nextIterable.currentPage().iterator().hasNext(); pageFuture.complete( new DefaultPage<>( currentPageElements, finalCurrentPageNumber, isLastPage)); } }); } return; } } if (iterable.hasMorePages()) { int finalCurrentPageNumber = currentPageNumber; int finalCurrentPageSize = currentPageSize; iterable .fetchNextPage() .whenComplete( (nextIterable, throwable) -> { if (throwable != null) { pageFuture.completeExceptionally(throwable); } else { getPage( nextIterable, targetPageNumber, finalCurrentPageNumber, finalCurrentPageSize, currentPageElements, pageFuture); } }); } else { // Reached the end of the result set, finish with what we have so far pageFuture.complete(new DefaultPage<>(currentPageElements, currentPageNumber, true)); } } private static class DefaultPage implements Page { private final List elements; private final int pageNumber; private final boolean isLast; DefaultPage(@NonNull List elements, int pageNumber, boolean isLast) { this.elements = ImmutableList.copyOf(elements); this.pageNumber = pageNumber; this.isLast = isLast; } @NonNull @Override public List getElements() { return elements; } @Override public int getPageNumber() { return pageNumber; } @Override public boolean isLast() { return isLast; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy