View Javadoc

1   /**
2    * Copyright 2007 The Apache Software Foundation
3    *
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS,
16   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17   * See the License for the specific language governing permissions and
18   * limitations under the License.
19   */
20  package org.apache.hadoop.hbase;
21  
22  import org.apache.commons.logging.Log;
23  import org.apache.commons.logging.LogFactory;
24  
25  import java.util.ConcurrentModificationException;
26  import java.util.HashMap;
27  import java.util.Map;
28  import java.util.concurrent.Delayed;
29  import java.util.concurrent.DelayQueue;
30  import java.util.concurrent.TimeUnit;
31  
32  import java.io.IOException;
33  
34  /**
35   * Leases
36   *
37   * There are several server classes in HBase that need to track external
38   * clients that occasionally send heartbeats.
39   *
40   * <p>These external clients hold resources in the server class.
41   * Those resources need to be released if the external client fails to send a
42   * heartbeat after some interval of time passes.
43   *
44   * <p>The Leases class is a general reusable class for this kind of pattern.
45   * An instance of the Leases class will create a thread to do its dirty work.
46   * You should close() the instance if you want to clean up the thread properly.
47   *
48   * <p>
49   * NOTE: This class extends Thread rather than Chore because the sleep time
50   * can be interrupted when there is something to do, rather than the Chore
51   * sleep time which is invariant.
52   */
53  public class Leases extends Thread {
54    private static final Log LOG = LogFactory.getLog(Leases.class.getName());
55    private final int leasePeriod;
56    private final int leaseCheckFrequency;
57    private volatile DelayQueue<Lease> leaseQueue = new DelayQueue<Lease>();
58    protected final Map<String, Lease> leases = new HashMap<String, Lease>();
59    private volatile boolean stopRequested = false;
60  
61    /**
62     * Creates a lease monitor
63     *
64     * @param leasePeriod - length of time (milliseconds) that the lease is valid
65     * @param leaseCheckFrequency - how often the lease should be checked
66     * (milliseconds)
67     */
68    public Leases(final int leasePeriod, final int leaseCheckFrequency) {
69      this.leasePeriod = leasePeriod;
70      this.leaseCheckFrequency = leaseCheckFrequency;
71    }
72  
73    /**
74     * @see java.lang.Thread#run()
75     */
76    @Override
77    public void run() {
78      while (!stopRequested || (stopRequested && leaseQueue.size() > 0) ) {
79        Lease lease = null;
80        try {
81          lease = leaseQueue.poll(leaseCheckFrequency, TimeUnit.MILLISECONDS);
82        } catch (InterruptedException e) {
83          continue;
84        } catch (ConcurrentModificationException e) {
85          continue;
86        } catch (Throwable e) {
87          LOG.fatal("Unexpected exception killed leases thread", e);
88          break;
89        }
90        if (lease == null) {
91          continue;
92        }
93        // A lease expired.  Run the expired code before removing from queue
94        // since its presence in queue is used to see if lease exists still.
95        if (lease.getListener() == null) {
96          LOG.error("lease listener is null for lease " + lease.getLeaseName());
97        } else {
98          lease.getListener().leaseExpired();
99        }
100       synchronized (leaseQueue) {
101         leases.remove(lease.getLeaseName());
102       }
103     }
104     close();
105   }
106 
107   /**
108    * Shuts down this lease instance when all outstanding leases expire.
109    * Like {@link #close()} but rather than violently end all leases, waits
110    * first on extant leases to finish.  Use this method if the lease holders
111    * could loose data, leak locks, etc.  Presumes client has shutdown
112    * allocation of new leases.
113    */
114   public void closeAfterLeasesExpire() {
115     this.stopRequested = true;
116   }
117 
118   /**
119    * Shut down this Leases instance.  All pending leases will be destroyed,
120    * without any cancellation calls.
121    */
122   public void close() {
123     LOG.info(Thread.currentThread().getName() + " closing leases");
124     this.stopRequested = true;
125     synchronized (leaseQueue) {
126       leaseQueue.clear();
127       leases.clear();
128       leaseQueue.notifyAll();
129     }
130     LOG.info(Thread.currentThread().getName() + " closed leases");
131   }
132 
133   /**
134    * Obtain a lease
135    *
136    * @param leaseName name of the lease
137    * @param listener listener that will process lease expirations
138    * @throws LeaseStillHeldException
139    */
140   public void createLease(String leaseName, final LeaseListener listener)
141   throws LeaseStillHeldException {
142     if (stopRequested) {
143       return;
144     }
145     Lease lease = new Lease(leaseName, listener,
146         System.currentTimeMillis() + leasePeriod);
147     synchronized (leaseQueue) {
148       if (leases.containsKey(leaseName)) {
149         throw new LeaseStillHeldException(leaseName);
150       }
151       leases.put(leaseName, lease);
152       leaseQueue.add(lease);
153     }
154   }
155 
156   /**
157    * Thrown if we are asked create a lease but lease on passed name already
158    * exists.
159    */
160   @SuppressWarnings("serial")
161   public static class LeaseStillHeldException extends IOException {
162     private final String leaseName;
163 
164     /**
165      * @param name
166      */
167     public LeaseStillHeldException(final String name) {
168       this.leaseName = name;
169     }
170 
171     /** @return name of lease */
172     public String getName() {
173       return this.leaseName;
174     }
175   }
176 
177   /**
178    * Renew a lease
179    *
180    * @param leaseName name of lease
181    * @throws LeaseException
182    */
183   public void renewLease(final String leaseName) throws LeaseException {
184     synchronized (leaseQueue) {
185       Lease lease = leases.get(leaseName);
186       // We need to check to see if the remove is successful as the poll in the run()
187       // method could have completed between the get and the remove which will result
188       // in a corrupt leaseQueue.
189       if (lease == null || !leaseQueue.remove(lease)) {
190         throw new LeaseException("lease '" + leaseName +
191                 "' does not exist or has already expired");
192       }
193       lease.setExpirationTime(System.currentTimeMillis() + leasePeriod);
194       leaseQueue.add(lease);
195     }
196   }
197 
198   /**
199    * Client explicitly cancels a lease.
200    *
201    * @param leaseName name of lease
202    * @throws LeaseException
203    */
204   public void cancelLease(final String leaseName) throws LeaseException {
205     synchronized (leaseQueue) {
206       Lease lease = leases.remove(leaseName);
207       if (lease == null) {
208         throw new LeaseException("lease '" + leaseName + "' does not exist");
209       }
210       leaseQueue.remove(lease);
211     }
212   }
213 
214   /** This class tracks a single Lease. */
215   private static class Lease implements Delayed {
216     private final String leaseName;
217     private final LeaseListener listener;
218     private long expirationTime;
219 
220     Lease(final String leaseName, LeaseListener listener, long expirationTime) {
221       this.leaseName = leaseName;
222       this.listener = listener;
223       this.expirationTime = expirationTime;
224     }
225 
226     /** @return the lease name */
227     public String getLeaseName() {
228       return leaseName;
229     }
230 
231     /** @return listener */
232     public LeaseListener getListener() {
233       return this.listener;
234     }
235 
236     @Override
237     public boolean equals(Object obj) {
238       if (this == obj) {
239         return true;
240       }
241       if (obj == null) {
242         return false;
243       }
244       if (getClass() != obj.getClass()) {
245         return false;
246       }
247       return this.hashCode() == ((Lease) obj).hashCode();
248     }
249 
250     @Override
251     public int hashCode() {
252       return this.leaseName.hashCode();
253     }
254 
255     public long getDelay(TimeUnit unit) {
256       return unit.convert(this.expirationTime - System.currentTimeMillis(),
257           TimeUnit.MILLISECONDS);
258     }
259 
260     public int compareTo(Delayed o) {
261       long delta = this.getDelay(TimeUnit.MILLISECONDS) -
262         o.getDelay(TimeUnit.MILLISECONDS);
263 
264       return this.equals(o) ? 0 : (delta > 0 ? 1 : -1);
265     }
266 
267     /** @param expirationTime the expirationTime to set */
268     public void setExpirationTime(long expirationTime) {
269       this.expirationTime = expirationTime;
270     }
271 
272     /**
273      * Get the expiration time for that lease
274      * @return expiration time
275      */
276     public long getExpirationTime() {
277       return this.expirationTime;
278     }
279 
280   }
281 }