/*
* (C) Copyright 2017 Nuxeo (http://nuxeo.com/) and others.
*
* Licensed 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.
*
* Contributors:
* Kevin Leturc
*/
package org.nuxeo.ecm.core.api;
import static org.nuxeo.ecm.core.api.ScrollResultImpl.emptyResult;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
* A cursor service which holds cursors on DB in order to perform scroll operations.
*
* @param <C> The cursor type.
* @param <O> The cursor item type.
* @since 9.1
*/
public class CursorService<C, O> {
private static final Log log = LogFactory.getLog(CursorService.class);
protected Map<String, CursorResult<C, O>> cursorResults = new ConcurrentHashMap<>();
public void checkForTimedOutScroll() {
cursorResults.entrySet().stream().forEach(e -> isScrollTimedOut(e.getKey(), e.getValue()));
}
protected boolean isScrollTimedOut(String scrollId, CursorResult<C, O> cursorResult) {
if (cursorResult.timedOut()) {
if (unregisterCursor(scrollId)) {
log.warn("Scroll '" + scrollId + "' timed out");
}
return true;
}
return false;
}
/**
* Registers the input {@link C} and generates a new <code>scrollId</code> to associate with.
*
* @return the scrollId associated to the cursor.
*/
public String registerCursor(C cursor, int batchSize, int keepAliveSeconds) {
return registerCursorResult(new CursorResult<>(cursor, batchSize, keepAliveSeconds));
}
/**
* Registers the input {@link C} associated to the input <code>scrollId</code>.
*
* @return the scrollId associated to the cursor.
*/
public String registerCursor(String scrollId, C cursor, int batchSize, int keepAliveSeconds) {
return registerCursorResult(scrollId, new CursorResult<>(cursor, batchSize, keepAliveSeconds));
}
/**
* Registers the input {@link CursorResult} and generates a new <code>scrollId</code> to associate with.
*
* @return the scrollId associated to the cursor result.
*/
public String registerCursorResult(CursorResult<C, O> cursorResult) {
String scrollId = UUID.randomUUID().toString();
return registerCursorResult(scrollId, cursorResult);
}
/**
* Registers the input {@link CursorResult} associated to the input <code>scrollId</code>.
*
* @return the scrollId associated to the cursor result.
*/
public String registerCursorResult(String scrollId, CursorResult<C, O> cursorResult) {
cursorResults.put(scrollId, cursorResult);
return scrollId;
}
/**
* Unregisters cursor associated to the input <code>scrollId</code>.
*
* @param scrollId The scoll id of {@link CursorResult} to unregister
* @return Whether or not the cursor was unregistered.
*/
public boolean unregisterCursor(String scrollId) {
CursorResult<C, O> cursorResult = cursorResults.remove(scrollId);
if (cursorResult != null) {
cursorResult.close();
return true;
}
return false;
}
/**
* @return the next batch of cursor associated to the input <code>scrollId</code>
*/
public ScrollResult scroll(String scrollId, Function<O, String> idExtractor) {
CursorResult<C, O> cursorResult = cursorResults.get(scrollId);
if (cursorResult == null) {
throw new NuxeoException("Unknown or timed out scrollId");
} else if (isScrollTimedOut(scrollId, cursorResult)) {
throw new NuxeoException("Timed out scrollId");
}
cursorResult.touch();
List<String> ids = new ArrayList<>(cursorResult.getBatchSize());
synchronized (cursorResult) {
if (!cursorResult.hasNext()) {
unregisterCursor(scrollId);
return emptyResult();
}
while (ids.size() < cursorResult.getBatchSize()) {
if (!cursorResult.hasNext()) {
// Don't unregister cursor here because we don't want scroll API to throw an exception during next
// call as it's a legitimate case - but close cursor
cursorResult.close();
break;
} else {
O obj = cursorResult.next();
String id = idExtractor.apply(obj);
if (id == null) {
log.error("Got a document without id: " + obj);
} else {
ids.add(id);
}
}
}
}
return new ScrollResultImpl(scrollId, ids);
}
/**
* Clear and close all cursors owned by this service.
*/
public void clear() {
Iterator<CursorResult<C, O>> values = cursorResults.values().iterator();
while (values.hasNext()) {
values.next().close();
values.remove();
}
}
}