View Javadoc

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