/*
*
* 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.util;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.apache.hadoop.classification.InterfaceAudience;
/**
* Allows multiple concurrent clients to lock on a numeric id with a minimal
* memory overhead. The intended usage is as follows:
*
* <pre>
* IdLock.Entry lockEntry = idLock.getLockEntry(id);
* try {
* // User code.
* } finally {
* idLock.releaseLockEntry(lockEntry);
* }</pre>
*/
@InterfaceAudience.Private
public class IdLock {
/** An entry returned to the client as a lock object */
public static class Entry {
private final long id;
private int numWaiters;
private boolean isLocked = true;
private Entry(long id) {
this.id = id;
}
public String toString() {
return "id=" + id + ", numWaiter=" + numWaiters + ", isLocked="
+ isLocked;
}
}
private ConcurrentMap<Long, Entry> map =
new ConcurrentHashMap<Long, Entry>();
/**
* Blocks until the lock corresponding to the given id is acquired.
*
* @param id an arbitrary number to lock on
* @return an "entry" to pass to {@link #releaseLockEntry(Entry)} to release
* the lock
* @throws IOException if interrupted
*/
public Entry getLockEntry(long id) throws IOException {
Entry entry = new Entry(id);
Entry existing;
while ((existing = map.putIfAbsent(entry.id, entry)) != null) {
synchronized (existing) {
if (existing.isLocked) {
++existing.numWaiters; // Add ourselves to waiters.
while (existing.isLocked) {
try {
existing.wait();
} catch (InterruptedException e) {
--existing.numWaiters; // Remove ourselves from waiters.
throw new InterruptedIOException(
"Interrupted waiting to acquire sparse lock");
}
}
--existing.numWaiters; // Remove ourselves from waiters.
existing.isLocked = true;
return existing;
}
// If the entry is not locked, it might already be deleted from the
// map, so we cannot return it. We need to get our entry into the map
// or get someone else's locked entry.
}
}
return entry;
}
/**
* Must be called in a finally block to decrease the internal counter and
* remove the monitor object for the given id if the caller is the last
* client.
*
* @param entry the return value of {@link #getLockEntry(long)}
*/
public void releaseLockEntry(Entry entry) {
synchronized (entry) {
entry.isLocked = false;
if (entry.numWaiters > 0) {
entry.notify();
} else {
map.remove(entry.id);
}
}
}
/** For testing */
void assertMapEmpty() {
assert map.size() == 0;
}
}