/* * 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.service.pager; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Iterator; import org.apache.cassandra.config.CFMetaData; import org.apache.cassandra.config.Schema; import org.apache.cassandra.db.*; import org.apache.cassandra.db.filter.ColumnCounter; import org.apache.cassandra.db.filter.IDiskAtomFilter; import org.apache.cassandra.exceptions.RequestExecutionException; import org.apache.cassandra.exceptions.RequestValidationException; abstract class AbstractQueryPager implements QueryPager { private final ConsistencyLevel consistencyLevel; private final boolean localQuery; protected final CFMetaData cfm; protected final IDiskAtomFilter columnFilter; private final long timestamp; private volatile int remaining; private volatile boolean exhausted; private volatile boolean lastWasRecorded; protected AbstractQueryPager(ConsistencyLevel consistencyLevel, int toFetch, boolean localQuery, String keyspace, String columnFamily, IDiskAtomFilter columnFilter, long timestamp) { this.consistencyLevel = consistencyLevel; this.localQuery = localQuery; this.cfm = Schema.instance.getCFMetaData(keyspace, columnFamily); this.columnFilter = columnFilter; this.timestamp = timestamp; this.remaining = toFetch; } public List<Row> fetchPage(int pageSize) throws RequestValidationException, RequestExecutionException { if (isExhausted()) return Collections.emptyList(); int currentPageSize = nextPageSize(pageSize); List<Row> rows = filterEmpty(queryNextPage(currentPageSize, consistencyLevel, localQuery)); if (rows.isEmpty()) { exhausted = true; return Collections.emptyList(); } int liveCount = getPageLiveCount(rows); remaining -= liveCount; // If we've got less than requested, there is no more query to do (but // we still need to return the current page) if (liveCount < currentPageSize) exhausted = true; // If it's not the first query and the first column is the last one returned (likely // but not certain since paging can race with deletes/expiration), then remove the // first column. if (containsPreviousLast(rows.get(0))) { rows = discardFirst(rows); remaining++; } // Otherwise, if 'lastWasRecorded', we queried for one more than the page size, // so if the page was is full, trim the last entry else if (lastWasRecorded && !exhausted) { // We've asked for one more than necessary rows = discardLast(rows); remaining++; } if (!isExhausted()) lastWasRecorded = recordLast(rows.get(rows.size() - 1)); return rows; } private List<Row> filterEmpty(List<Row> result) { for (Row row : result) { if (row.cf == null || row.cf.getColumnCount() == 0) { List<Row> newResult = new ArrayList<Row>(result.size() - 1); for (Row row2 : result) { if (row2.cf == null || row2.cf.getColumnCount() == 0) continue; newResult.add(row2); } return newResult; } } return result; } protected void restoreState(int remaining, boolean lastWasRecorded) { this.remaining = remaining; this.lastWasRecorded = lastWasRecorded; } public boolean isExhausted() { return exhausted || remaining == 0; } public int maxRemaining() { return remaining; } public long timestamp() { return timestamp; } private int nextPageSize(int pageSize) { return Math.min(remaining, pageSize) + (lastWasRecorded ? 1 : 0); } public ColumnCounter columnCounter() { return columnFilter.columnCounter(cfm.comparator, timestamp); } protected abstract List<Row> queryNextPage(int pageSize, ConsistencyLevel consistency, boolean localQuery) throws RequestValidationException, RequestExecutionException; protected abstract boolean containsPreviousLast(Row first); protected abstract boolean recordLast(Row last); private List<Row> discardFirst(List<Row> rows) { Row first = rows.get(0); ColumnFamily newCf = discardFirst(first.cf); int count = newCf.getColumnCount(); List<Row> newRows = new ArrayList<Row>(count == 0 ? rows.size() - 1 : rows.size()); if (count != 0) newRows.add(new Row(first.key, newCf)); newRows.addAll(rows.subList(1, rows.size())); return newRows; } private List<Row> discardLast(List<Row> rows) { Row last = rows.get(rows.size() - 1); ColumnFamily newCf = discardLast(last.cf); int count = newCf.getColumnCount(); List<Row> newRows = new ArrayList<Row>(count == 0 ? rows.size() - 1 : rows.size()); newRows.addAll(rows.subList(0, rows.size() - 1)); if (count != 0) newRows.add(new Row(last.key, newCf)); return newRows; } private int getPageLiveCount(List<Row> page) { int count = 0; for (Row row : page) count += columnCounter().countAll(row.cf).live(); return count; } private ColumnFamily discardFirst(ColumnFamily cf) { ColumnFamily copy = cf.cloneMeShallow(); ColumnCounter counter = columnCounter(); Iterator<Column> iter = cf.iterator(); DeletionInfo.InOrderTester tester = cf.inOrderDeletionTester(); // Discard the first live while (iter.hasNext()) { Column c = iter.next(); counter.count(c, tester); if (counter.live() > 1) { copy.addColumn(c); while (iter.hasNext()) copy.addColumn(iter.next()); } } return copy; } private ColumnFamily discardLast(ColumnFamily cf) { ColumnFamily copy = cf.cloneMeShallow(); // Redoing the counting like that is not extremely efficient, but // discardLast is only called in case of a race between paging and // a deletion, which is pretty unlikely, so probably not a big deal int liveCount = columnCounter().countAll(cf).live(); ColumnCounter counter = columnCounter(); DeletionInfo.InOrderTester tester = cf.inOrderDeletionTester(); // Discard the first live for (Column c : cf) { counter.count(c, tester); if (counter.live() < liveCount) copy.addColumn(c); } return copy; } protected static ByteBuffer firstName(ColumnFamily cf) { return cf.iterator().next().name(); } protected static ByteBuffer lastName(ColumnFamily cf) { return cf.getReverseSortedColumns().iterator().next().name(); } }