/* * 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.lucene.replicator; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import org.apache.lucene.store.AlreadyClosedException; /** * A {@link Replicator} implementation for use by the side that publishes * {@link Revision}s, as well for clients to {@link #checkForUpdate(String) * check for updates}. When a client needs to be updated, it is returned a * {@link SessionToken} through which it can * {@link #obtainFile(String, String, String) obtain} the files of that * revision. As long as a revision is being replicated, this replicator * guarantees that it will not be {@link Revision#release() released}. * <p> * Replication sessions expire by default after * {@link #DEFAULT_SESSION_EXPIRATION_THRESHOLD}, and the threshold can be * configured through {@link #setExpirationThreshold(long)}. * * @lucene.experimental */ public class LocalReplicator implements Replicator { private static class RefCountedRevision { private final AtomicInteger refCount = new AtomicInteger(1); public final Revision revision; public RefCountedRevision(Revision revision) { this.revision = revision; } public void decRef() throws IOException { if (refCount.get() <= 0) { throw new IllegalStateException("this revision is already released"); } final int rc = refCount.decrementAndGet(); if (rc == 0) { boolean success = false; try { revision.release(); success = true; } finally { if (!success) { // Put reference back on failure refCount.incrementAndGet(); } } } else if (rc < 0) { throw new IllegalStateException("too many decRef calls: refCount is " + rc + " after decrement"); } } public void incRef() { refCount.incrementAndGet(); } } private static class ReplicationSession { public final SessionToken session; public final RefCountedRevision revision; private volatile long lastAccessTime; ReplicationSession(SessionToken session, RefCountedRevision revision) { this.session = session; this.revision = revision; lastAccessTime = TimeUnit.MILLISECONDS.convert(System.nanoTime(), TimeUnit.NANOSECONDS); } boolean isExpired(long expirationThreshold) { return lastAccessTime < (TimeUnit.MILLISECONDS.convert(System.nanoTime(), TimeUnit.NANOSECONDS) - expirationThreshold); } void markAccessed() { lastAccessTime = TimeUnit.MILLISECONDS.convert(System.nanoTime(), TimeUnit.NANOSECONDS); } } /** Threshold for expiring inactive sessions. Defaults to 30 minutes. */ public static final long DEFAULT_SESSION_EXPIRATION_THRESHOLD = 1000 * 60 * 30; private long expirationThresholdMilllis = LocalReplicator.DEFAULT_SESSION_EXPIRATION_THRESHOLD; private volatile RefCountedRevision currentRevision; private volatile boolean closed = false; private final AtomicInteger sessionToken = new AtomicInteger(0); private final Map<String, ReplicationSession> sessions = new HashMap<>(); private void checkExpiredSessions() throws IOException { // make a "to-delete" list so we don't risk deleting from the map while iterating it final ArrayList<ReplicationSession> toExpire = new ArrayList<>(); for (ReplicationSession token : sessions.values()) { if (token.isExpired(expirationThresholdMilllis)) { toExpire.add(token); } } for (ReplicationSession token : toExpire) { releaseSession(token.session.id); } } private void releaseSession(String sessionID) throws IOException { ReplicationSession session = sessions.remove(sessionID); // if we're called concurrently by close() and release(), could be that one // thread beats the other to release the session. if (session != null) { session.revision.decRef(); } } /** Ensure that replicator is still open, or throw {@link AlreadyClosedException} otherwise. */ protected final synchronized void ensureOpen() { if (closed) { throw new AlreadyClosedException("This replicator has already been closed"); } } @Override public synchronized SessionToken checkForUpdate(String currentVersion) { ensureOpen(); if (currentRevision == null) { // no published revisions yet return null; } if (currentVersion != null && currentRevision.revision.compareTo(currentVersion) <= 0) { // currentVersion is newer or equal to latest published revision return null; } // currentVersion is either null or older than latest published revision currentRevision.incRef(); final String sessionID = Integer.toString(sessionToken.incrementAndGet()); final SessionToken sessionToken = new SessionToken(sessionID, currentRevision.revision); final ReplicationSession timedSessionToken = new ReplicationSession(sessionToken, currentRevision); sessions.put(sessionID, timedSessionToken); return sessionToken; } @Override public synchronized void close() throws IOException { if (!closed) { // release all managed revisions for (ReplicationSession session : sessions.values()) { session.revision.decRef(); } sessions.clear(); closed = true; } } /** * Returns the expiration threshold. * * @see #setExpirationThreshold(long) */ public long getExpirationThreshold() { return expirationThresholdMilllis; } @Override public synchronized InputStream obtainFile(String sessionID, String source, String fileName) throws IOException { ensureOpen(); ReplicationSession session = sessions.get(sessionID); if (session != null && session.isExpired(expirationThresholdMilllis)) { releaseSession(sessionID); session = null; } // session either previously expired, or we just expired it if (session == null) { throw new SessionExpiredException("session (" + sessionID + ") expired while obtaining file: source=" + source + " file=" + fileName); } sessions.get(sessionID).markAccessed(); return session.revision.revision.open(source, fileName); } @Override public synchronized void publish(Revision revision) throws IOException { ensureOpen(); if (currentRevision != null) { int compare = revision.compareTo(currentRevision.revision); if (compare == 0) { // same revision published again, ignore but release it revision.release(); return; } if (compare < 0) { revision.release(); throw new IllegalArgumentException("Cannot publish an older revision: rev=" + revision + " current=" + currentRevision); } } // swap revisions final RefCountedRevision oldRevision = currentRevision; currentRevision = new RefCountedRevision(revision); if (oldRevision != null) { oldRevision.decRef(); } // check for expired sessions checkExpiredSessions(); } @Override public synchronized void release(String sessionID) throws IOException { ensureOpen(); releaseSession(sessionID); } /** * Modify session expiration time - if a replication session is inactive that * long it is automatically expired, and further attempts to operate within * this session will throw a {@link SessionExpiredException}. */ public synchronized void setExpirationThreshold(long expirationThreshold) throws IOException { ensureOpen(); this.expirationThresholdMilllis = expirationThreshold; checkExpiredSessions(); } }