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 }