/** * * 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.hadoop.hbase.regionserver; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.hbase.util.HasThread; import java.util.ConcurrentModificationException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Delayed; import java.util.concurrent.DelayQueue; import java.util.concurrent.TimeUnit; import java.io.IOException; /** * Leases * * There are several server classes in HBase that need to track external * clients that occasionally send heartbeats. * * <p>These external clients hold resources in the server class. * Those resources need to be released if the external client fails to send a * heartbeat after some interval of time passes. * * <p>The Leases class is a general reusable class for this kind of pattern. * An instance of the Leases class will create a thread to do its dirty work. * You should close() the instance if you want to clean up the thread properly. * * <p> * NOTE: This class extends Thread rather than Chore because the sleep time * can be interrupted when there is something to do, rather than the Chore * sleep time which is invariant. */ @InterfaceAudience.Private public class Leases extends HasThread { private static final Log LOG = LogFactory.getLog(Leases.class.getName()); private final int leaseCheckFrequency; private volatile DelayQueue<Lease> leaseQueue = new DelayQueue<Lease>(); protected final Map<String, Lease> leases = new HashMap<String, Lease>(); private volatile boolean stopRequested = false; /** * Creates a lease monitor * * @param leaseCheckFrequency - how often the lease should be checked * (milliseconds) */ public Leases(final int leaseCheckFrequency) { this.leaseCheckFrequency = leaseCheckFrequency; setDaemon(true); } /** * @see java.lang.Thread#run() */ @Override public void run() { while (!stopRequested || (stopRequested && leaseQueue.size() > 0) ) { Lease lease = null; try { lease = leaseQueue.poll(leaseCheckFrequency, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { continue; } catch (ConcurrentModificationException e) { continue; } catch (Throwable e) { LOG.fatal("Unexpected exception killed leases thread", e); break; } if (lease == null) { continue; } // A lease expired. Run the expired code before removing from queue // since its presence in queue is used to see if lease exists still. if (lease.getListener() == null) { LOG.error("lease listener is null for lease " + lease.getLeaseName()); } else { lease.getListener().leaseExpired(); } synchronized (leaseQueue) { leases.remove(lease.getLeaseName()); } } close(); } /** * Shuts down this lease instance when all outstanding leases expire. * Like {@link #close()} but rather than violently end all leases, waits * first on extant leases to finish. Use this method if the lease holders * could loose data, leak locks, etc. Presumes client has shutdown * allocation of new leases. */ public void closeAfterLeasesExpire() { this.stopRequested = true; } /** * Shut down this Leases instance. All pending leases will be destroyed, * without any cancellation calls. */ public void close() { LOG.info(Thread.currentThread().getName() + " closing leases"); this.stopRequested = true; synchronized (leaseQueue) { leaseQueue.clear(); leases.clear(); leaseQueue.notifyAll(); } LOG.info(Thread.currentThread().getName() + " closed leases"); } /** * Obtain a lease. * * @param leaseName name of the lease * @param leaseTimeoutPeriod length of the lease in milliseconds * @param listener listener that will process lease expirations * @throws LeaseStillHeldException */ public void createLease(String leaseName, int leaseTimeoutPeriod, final LeaseListener listener) throws LeaseStillHeldException { addLease(new Lease(leaseName, leaseTimeoutPeriod, listener)); } /** * Inserts lease. Resets expiration before insertion. * @param lease * @throws LeaseStillHeldException */ public void addLease(final Lease lease) throws LeaseStillHeldException { if (this.stopRequested) { return; } lease.resetExpirationTime(); synchronized (leaseQueue) { if (leases.containsKey(lease.getLeaseName())) { throw new LeaseStillHeldException(lease.getLeaseName()); } leases.put(lease.getLeaseName(), lease); leaseQueue.add(lease); } } /** * Thrown if we are asked create a lease but lease on passed name already * exists. */ @SuppressWarnings("serial") public static class LeaseStillHeldException extends IOException { private final String leaseName; /** * @param name */ public LeaseStillHeldException(final String name) { this.leaseName = name; } /** @return name of lease */ public String getName() { return this.leaseName; } } /** * Renew a lease * * @param leaseName name of lease * @throws LeaseException */ public void renewLease(final String leaseName) throws LeaseException { synchronized (leaseQueue) { Lease lease = leases.get(leaseName); // We need to check to see if the remove is successful as the poll in the run() // method could have completed between the get and the remove which will result // in a corrupt leaseQueue. if (lease == null || !leaseQueue.remove(lease)) { throw new LeaseException("lease '" + leaseName + "' does not exist or has already expired"); } lease.resetExpirationTime(); leaseQueue.add(lease); } } /** * Client explicitly cancels a lease. * @param leaseName name of lease * @throws LeaseException */ public void cancelLease(final String leaseName) throws LeaseException { removeLease(leaseName); } /** * Remove named lease. * Lease is removed from the list of leases and removed from the delay queue. * Lease can be resinserted using {@link #addLease(Lease)} * * @param leaseName name of lease * @throws LeaseException * @return Removed lease */ Lease removeLease(final String leaseName) throws LeaseException { Lease lease = null; synchronized (leaseQueue) { lease = leases.remove(leaseName); if (lease == null) { throw new LeaseException("lease '" + leaseName + "' does not exist"); } leaseQueue.remove(lease); } return lease; } /** This class tracks a single Lease. */ static class Lease implements Delayed { private final String leaseName; private final LeaseListener listener; private int leaseTimeoutPeriod; private long expirationTime; Lease(final String leaseName, int leaseTimeoutPeriod, LeaseListener listener) { this.leaseName = leaseName; this.listener = listener; this.leaseTimeoutPeriod = leaseTimeoutPeriod; this.expirationTime = 0; } /** @return the lease name */ public String getLeaseName() { return leaseName; } /** @return listener */ public LeaseListener getListener() { return this.listener; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } return this.hashCode() == ((Lease) obj).hashCode(); } @Override public int hashCode() { return this.leaseName.hashCode(); } public long getDelay(TimeUnit unit) { return unit.convert(this.expirationTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } public int compareTo(Delayed o) { long delta = this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS); return this.equals(o) ? 0 : (delta > 0 ? 1 : -1); } /** * Resets the expiration time of the lease. */ public void resetExpirationTime() { this.expirationTime = System.currentTimeMillis() + this.leaseTimeoutPeriod; } } }