/*
* HA-JDBC: High-Availability JDBC
* Copyright (C) 2012 Paul Ferraro
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sf.hajdbc.lock.distributed;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import net.sf.hajdbc.Database;
import net.sf.hajdbc.DatabaseCluster;
import net.sf.hajdbc.distributed.Command;
import net.sf.hajdbc.distributed.CommandDispatcher;
import net.sf.hajdbc.distributed.CommandDispatcherFactory;
import net.sf.hajdbc.distributed.CommandResponse;
import net.sf.hajdbc.distributed.Member;
import net.sf.hajdbc.distributed.MembershipListener;
import net.sf.hajdbc.distributed.Remote;
import net.sf.hajdbc.distributed.Stateful;
import net.sf.hajdbc.lock.LockManager;
import net.sf.hajdbc.logging.Level;
import net.sf.hajdbc.logging.Logger;
import net.sf.hajdbc.logging.LoggerFactory;
import net.sf.hajdbc.messages.Messages;
import net.sf.hajdbc.messages.MessagesFactory;
import net.sf.hajdbc.util.Objects;
/**
* @author Paul Ferraro
*/
public class DistributedLockManager implements LockManager, LockCommandContext, Stateful, MembershipListener
{
static final Logger logger = LoggerFactory.getLogger(DistributedLockManager.class);
static final Messages messages = MessagesFactory.getMessages();
final CommandDispatcher<LockCommandContext> dispatcher;
private final LockManager lockManager;
private final ConcurrentMap<Member, Map<LockDescriptor, Lock>> remoteLockDescriptorMap = new ConcurrentHashMap<>();
public <Z, D extends Database<Z>> DistributedLockManager(DatabaseCluster<Z, D> cluster, CommandDispatcherFactory dispatcherFactory) throws Exception
{
this.lockManager = cluster.getLockManager();
LockCommandContext context = this;
this.dispatcher = dispatcherFactory.createCommandDispatcher(cluster.getId() + ".lock", context, this, this);
}
/**
* {@inheritDoc}
* @see net.sf.hajdbc.lock.LockManager#readLock(java.lang.String)
*/
@Override
public Lock readLock(String id)
{
return this.lockManager.readLock(id);
}
/**
* {@inheritDoc}
* @see net.sf.hajdbc.lock.LockManager#writeLock(java.lang.String)
*/
@Override
public Lock writeLock(String id)
{
RemoteLockDescriptor descriptor = new RemoteLockDescriptorImpl(id, LockType.WRITE, this.dispatcher.getLocal());
return new DistributedLock(descriptor, this.getLock(descriptor), this.dispatcher);
}
/**
* {@inheritDoc}
* @see net.sf.hajdbc.lock.distributed.LockCommandContext#getLock(net.sf.hajdbc.lock.distributed.LockDescriptor)
*/
@Override
public Lock getLock(LockDescriptor lock)
{
String id = lock.getId();
switch (lock.getType())
{
case READ:
{
return this.lockManager.readLock(id);
}
case WRITE:
{
return this.lockManager.writeLock(id);
}
default:
{
throw new IllegalStateException();
}
}
}
/**
* {@inheritDoc}
* @see net.sf.hajdbc.Lifecycle#start()
*/
@Override
public void start() throws SQLException
{
this.lockManager.start();
this.dispatcher.start();
}
/**
* {@inheritDoc}
* @see net.sf.hajdbc.Lifecycle#stop()
*/
@Override
public void stop()
{
this.dispatcher.stop();
this.lockManager.stop();
}
/**
* {@inheritDoc}
* @see net.sf.hajdbc.lock.distributed.LockCommandContext#getRemoteLocks(net.sf.hajdbc.distributed.Remote)
*/
@Override
public Map<LockDescriptor, Lock> getRemoteLocks(Remote remote)
{
return this.remoteLockDescriptorMap.get(remote.getMember());
}
/**
* {@inheritDoc}
* @see net.sf.hajdbc.distributed.Stateful#writeState(java.io.ObjectOutput)
*/
@Override
public void writeState(ObjectOutput output) throws IOException
{
output.writeInt(this.remoteLockDescriptorMap.size());
for (Map.Entry<Member, Map<LockDescriptor, Lock>> entry: this.remoteLockDescriptorMap.entrySet())
{
output.writeObject(entry.getKey());
Map<LockDescriptor, Lock> locks = entry.getValue();
synchronized (locks)
{
Set<LockDescriptor> descriptors = locks.keySet();
output.writeInt(descriptors.size());
for (LockDescriptor descriptor: descriptors)
{
output.writeUTF(descriptor.getId());
output.writeByte(descriptor.getType().ordinal());
}
}
}
}
/**
* {@inheritDoc}
* @see net.sf.hajdbc.distributed.Stateful#readState(java.io.ObjectInput)
*/
@Override
public void readState(ObjectInput input) throws IOException
{
// Discard any previous state
this.remoteLockDescriptorMap.clear();
int size = input.readInt();
LockType[] types = LockType.values();
for (int i = 0; i < size; ++i)
{
Member member = Objects.readObject(input, Member.class);
Map<LockDescriptor, Lock> map = new HashMap<>();
int locks = input.readInt();
for (int j = 0; j < locks; ++j)
{
String id = input.readUTF();
LockType type = types[input.readByte()];
LockDescriptor descriptor = new RemoteLockDescriptorImpl(id, type, member);
Lock lock = this.getLock(descriptor);
lock.lock();
map.put(descriptor, lock);
}
this.remoteLockDescriptorMap.put(member, map);
}
}
/**
* {@inheritDoc}
* @see net.sf.hajdbc.distributed.MembershipListener#added(net.sf.hajdbc.distributed.Member)
*/
@Override
public void added(Member member)
{
this.remoteLockDescriptorMap.putIfAbsent(member, new HashMap<LockDescriptor, Lock>());
}
/**
* {@inheritDoc}
* @see net.sf.hajdbc.distributed.MembershipListener#removed(net.sf.hajdbc.distributed.Member)
*/
@Override
public void removed(Member member)
{
Map<LockDescriptor, Lock> locks = this.remoteLockDescriptorMap.remove(member);
if (locks != null)
{
for (Lock lock: locks.values())
{
lock.unlock();
}
}
}
private static class DistributedLock implements Lock
{
private static final int[] BACKOFF_INTERVALS = new int[] { 1, 10, 100 };
private final RemoteLockDescriptor descriptor;
private final Lock lock;
private final CommandDispatcher<LockCommandContext> dispatcher;
DistributedLock(RemoteLockDescriptor descriptor, Lock lock, CommandDispatcher<LockCommandContext> dispatcher)
{
this.descriptor = descriptor;
this.lock = lock;
this.dispatcher = dispatcher;
}
private static void sleep(int retry) throws InterruptedException
{
if (retry > 0)
{
Thread.sleep(BACKOFF_INTERVALS[Math.min(retry, BACKOFF_INTERVALS.length) - 1]);
}
}
@Override
public void lock()
{
boolean locked = false;
int retry = 0;
while (!locked)
{
try
{
sleep(retry);
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
}
Member coordinator = this.dispatcher.getCoordinator();
if (this.dispatcher.getLocal().equals(coordinator))
{
this.lock.lock();
try
{
locked = this.lockMembers(coordinator);
}
finally
{
if (!locked)
{
this.lock.unlock();
}
}
}
else
{
locked = this.lockFromNonCoordinator(coordinator, Long.MAX_VALUE);
}
retry += 1;
}
}
@Override
public void lockInterruptibly() throws InterruptedException
{
boolean locked = false;
int retry = 0;
while (!locked)
{
sleep(retry);
Member coordinator = this.dispatcher.getCoordinator();
if (this.dispatcher.getLocal().equals(coordinator))
{
this.lock.lockInterruptibly();
try
{
locked = this.lockMembers(coordinator);
}
finally
{
if (!locked)
{
this.lock.unlock();
}
}
}
else
{
this.lockFromNonCoordinator(coordinator, Long.MAX_VALUE);
}
if (Thread.currentThread().isInterrupted())
{
throw new InterruptedException();
}
retry += 1;
}
}
@Override
public boolean tryLock()
{
boolean locked = false;
int retry = 0;
try
{
while (!locked && (retry <= BACKOFF_INTERVALS.length))
{
sleep(retry);
Member coordinator = this.dispatcher.getCoordinator();
if (this.dispatcher.getLocal().equals(coordinator))
{
if (this.lock.tryLock())
{
try
{
locked = this.lockMembers(coordinator);
}
finally
{
if (!locked)
{
this.lock.unlock();
}
}
}
}
else
{
locked = this.lockFromNonCoordinator(coordinator, 0);
}
retry += 1;
}
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
}
return locked;
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException
{
boolean locked = false;
long start = System.currentTimeMillis();
long stop = start + TimeUnit.MILLISECONDS.convert(time, unit);
long now = start;
int retry = 0;
try
{
while (!locked && (now <= stop))
{
sleep(retry);
Member coordinator = this.dispatcher.getCoordinator();
long timeout = stop - now;
if (this.dispatcher.getLocal().equals(coordinator))
{
if (this.lock.tryLock(timeout, TimeUnit.MILLISECONDS))
{
try
{
locked = this.lockMembers(coordinator);
}
finally
{
if (!locked)
{
this.lock.unlock();
}
}
}
}
else
{
locked = this.lockFromNonCoordinator(coordinator, timeout);
}
now = System.currentTimeMillis();
retry += 1;
}
}
catch (InterruptedException e)
{
Thread.currentThread().interrupt();
}
return locked;
}
private boolean lockFromNonCoordinator(Member coordinator, long timeout)
{
boolean locked = false;
if (this.lockCoordinator(coordinator, timeout))
{
try
{
locked = this.lockMembers(coordinator);
}
finally
{
if (!locked)
{
this.unlock(coordinator);
}
}
}
return locked;
}
private boolean lockMembers(Member coordinator)
{
Command<Boolean, LockCommandContext> command = new AcquireLockCommand(this.descriptor, 0);
try
{
Map<Member, CommandResponse<Boolean>> results = this.dispatcher.executeAll(command, coordinator);
List<Member> lockedMembers = new ArrayList<>(results.size());
for (Map.Entry<Member, CommandResponse<Boolean>> entry: results.entrySet())
{
Member member = entry.getKey();
if (readAcquireResponse(command, member, entry.getValue()))
{
lockedMembers.add(member);
}
}
boolean locked = lockedMembers.size() == results.size();
if (!locked)
{
for (Member member: lockedMembers)
{
this.unlock(member);
}
}
return locked;
}
catch (Exception e)
{
logger.log(Level.WARN, e, messages.sendCommandToClusterFailed(command));
return false;
}
}
private boolean lockCoordinator(Member coordinator, long timeout)
{
Command<Boolean, LockCommandContext> command = new AcquireLockCommand(this.descriptor, timeout);
try
{
CommandResponse<Boolean> response = this.dispatcher.execute(new AcquireLockCommand(this.descriptor, timeout), coordinator);
return readAcquireResponse(command, coordinator, response);
}
catch (Exception e)
{
logger.log(Level.WARN, e, messages.sendCommandToMemberFailed(command, coordinator));
return false;
}
}
private static boolean readAcquireResponse(Command<Boolean, LockCommandContext> command, Member member, CommandResponse<Boolean> response)
{
return readResponse(command, member, response, Boolean.FALSE).booleanValue();
}
@Override
public void unlock()
{
Member coordinator = this.dispatcher.getCoordinator();
this.unlockMembers(coordinator);
if (this.dispatcher.getLocal().equals(coordinator))
{
this.lock.unlock();
}
else
{
this.unlock(coordinator);
}
}
private void unlockMembers(Member... excluded)
{
Command<Void, LockCommandContext> command = new ReleaseLockCommand(this.descriptor);
try
{
Map<Member, CommandResponse<Void>> responses = this.dispatcher.executeAll(command, excluded);
for (Map.Entry<Member, CommandResponse<Void>> entry: responses.entrySet())
{
readReleaseResponse(command, entry.getKey(), entry.getValue());
}
}
catch (Exception e)
{
logger.log(Level.WARN, e, messages.sendCommandToClusterFailed(command));
}
}
private void unlock(Member member)
{
Command<Void, LockCommandContext> command = new ReleaseLockCommand(this.descriptor);
try
{
CommandResponse<Void> response = this.dispatcher.execute(command, member);
readReleaseResponse(command, member, response);
}
catch (Exception e)
{
logger.log(Level.WARN, e, messages.sendCommandToMemberFailed(command, member));
}
}
private static void readReleaseResponse(Command<Void, LockCommandContext> command, Member member, CommandResponse<Void> response)
{
readResponse(command, member, response, null);
}
private static <R> R readResponse(Command<R, LockCommandContext> command, Member member, CommandResponse<R> response, R failureResult)
{
try
{
return response.get();
}
catch (Exception e)
{
logger.log(Level.WARN, e, messages.executeCommandFailed(command, member));
return failureResult;
}
}
@Override
public Condition newCondition()
{
throw new UnsupportedOperationException();
}
}
private static class RemoteLockDescriptorImpl implements RemoteLockDescriptor
{
private static final long serialVersionUID = 1950781245453120790L;
private final String id;
private transient LockType type;
private final Member member;
RemoteLockDescriptorImpl(String id, LockType type, Member member)
{
this.id = id;
this.type = type;
this.member = member;
}
@Override
public String getId()
{
return this.id;
}
@Override
public LockType getType()
{
return this.type;
}
@Override
public Member getMember()
{
return this.member;
}
private void writeObject(ObjectOutputStream out) throws IOException
{
out.defaultWriteObject();
out.writeByte(this.type.ordinal());
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
{
in.defaultReadObject();
this.type = LockType.values()[in.readByte()];
}
/**
* {@inheritDoc}
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object object)
{
if ((object == null) || !(object instanceof RemoteLockDescriptor)) return false;
String id = ((RemoteLockDescriptor) object).getId();
return ((this.id != null) && (id != null)) ? this.id.equals(id) : (this.id == id);
}
/**
* {@inheritDoc}
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode()
{
return this.id != null ? this.id.hashCode() : 0;
}
/**
* {@inheritDoc}
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
return String.format("%sLock(%s)", this.type.name().toLowerCase(), (this.id != null) ? this.id : "");
}
}
}