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.util;
20  
21  import java.io.IOException;
22  import java.io.InterruptedIOException;
23  import java.util.concurrent.ConcurrentHashMap;
24  import java.util.concurrent.ConcurrentMap;
25  
26  import org.apache.hadoop.hbase.classification.InterfaceAudience;
27  
28  /**
29   * Allows multiple concurrent clients to lock on a numeric id with a minimal
30   * memory overhead. The intended usage is as follows:
31   *
32   * <pre>
33   * IdLock.Entry lockEntry = idLock.getLockEntry(id);
34   * try {
35   *   // User code.
36   * } finally {
37   *   idLock.releaseLockEntry(lockEntry);
38   * }</pre>
39   */
40  @InterfaceAudience.Private
41  public class IdLock {
42  
43    /** An entry returned to the client as a lock object */
44    public static class Entry {
45      private final long id;
46      private int numWaiters;
47      private boolean isLocked = true;
48  
49      private Entry(long id) {
50        this.id = id;
51      }
52  
53      public String toString() {
54        return "id=" + id + ", numWaiter=" + numWaiters + ", isLocked="
55            + isLocked;
56      }
57    }
58  
59    private ConcurrentMap<Long, Entry> map =
60        new ConcurrentHashMap<Long, Entry>();
61  
62    /**
63     * Blocks until the lock corresponding to the given id is acquired.
64     *
65     * @param id an arbitrary number to lock on
66     * @return an "entry" to pass to {@link #releaseLockEntry(Entry)} to release
67     *         the lock
68     * @throws IOException if interrupted
69     */
70    public Entry getLockEntry(long id) throws IOException {
71      Entry entry = new Entry(id);
72      Entry existing;
73      while ((existing = map.putIfAbsent(entry.id, entry)) != null) {
74        synchronized (existing) {
75          if (existing.isLocked) {
76            ++existing.numWaiters;  // Add ourselves to waiters.
77            while (existing.isLocked) {
78              try {
79                existing.wait();
80              } catch (InterruptedException e) {
81                --existing.numWaiters;  // Remove ourselves from waiters.
82                // HBASE-21292/HBASE-22706
83                // There is a rare case that interrupting and the lock owner thread call
84                // releaseLockEntry at the same time. Since the owner thread found there
85                // still one waiting, it won't remove the entry from the map. If the interrupted
86                // thread is the last one waiting on the lock, and since an exception is thrown,
87                // the 'existing' entry will stay in the map forever. Later threads which try to
88                // get this lock will stuck in a infinite loop because
89                // existing = map.putIfAbsent(entry.id, entry)) != null and existing.isLocked=false.
90                if (!existing.isLocked && existing.numWaiters == 0) {
91                  map.remove(existing.id);
92                }
93                throw new InterruptedIOException(
94                    "Interrupted waiting to acquire sparse lock");
95              }
96            }
97  
98            --existing.numWaiters;  // Remove ourselves from waiters.
99            existing.isLocked = true;
100           return existing;
101         }
102         // If the entry is not locked, it might already be deleted from the
103         // map, so we cannot return it. We need to get our entry into the map
104         // or get someone else's locked entry.
105       }
106     }
107     return entry;
108   }
109 
110   /**
111    * Must be called in a finally block to decrease the internal counter and
112    * remove the monitor object for the given id if the caller is the last
113    * client.
114    *
115    * @param entry the return value of {@link #getLockEntry(long)}
116    */
117   public void releaseLockEntry(Entry entry) {
118     synchronized (entry) {
119       entry.isLocked = false;
120       if (entry.numWaiters > 0) {
121         entry.notify();
122       } else {
123         map.remove(entry.id);
124       }
125     }
126   }
127 
128   /** For testing */
129   void assertMapEmpty() {
130     assert map.size() == 0;
131   }
132 
133   public void waitForWaiters(long id, int numWaiters) throws InterruptedException {
134     for (Entry entry;;) {
135       entry = map.get(id);
136       if (entry != null) {
137         synchronized (entry) {
138           if (entry.numWaiters >= numWaiters) {
139             return;
140           }
141         }
142       }
143       Thread.sleep(100);
144     }
145   }
146 }