Java tutorial
/* * jGnash, a personal finance application * Copyright (C) 2001-2015 Craig Cavanaugh * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package jgnash.engine.concurrent; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import io.netty.bootstrap.Bootstrap; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.DelimiterBasedFrameDecoder; import io.netty.handler.codec.Delimiters; import io.netty.handler.codec.string.StringDecoder; import io.netty.handler.codec.string.StringEncoder; import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; import java.text.MessageFormat; import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantReadWriteLock; import java.util.logging.Level; import java.util.logging.Logger; import jgnash.net.ConnectionFactory; import jgnash.util.EncryptionManager; import jgnash.util.NotNull; /** * Lock manager for distributed engine instances * * @author Craig Cavanaugh */ public class DistributedLockManager implements LockManager { private static final Logger logger = Logger.getLogger(DistributedLockManager.class.getName()); private final Map<String, DistributedReadWriteLock> lockMap = new ConcurrentHashMap<>(); private final Map<String, CountDownLatch> latchMap = new HashMap<>(); private final Lock latchLock = new ReentrantLock(); /** * lock_action, lock_id, thread_id, lock_type */ private static final String PATTERN = "{0},{1},{2},{3}"; static final String UUID_PREFIX = "UUID:"; private NioEventLoopGroup eventLoopGroup; private final int port; private final String host; private Channel channel; private static final String EOL_DELIMITER = "\r\n"; private final ExecutorService executorService = Executors.newCachedThreadPool(); private EncryptionManager encryptionManager = null; /** * Unique id to differentiate remote threads */ private static final String uuid = UUID.randomUUID().toString(); static { logger.setLevel(Level.INFO); } public DistributedLockManager(final String host, final int port) { this.host = host; this.port = port; } private String encrypt(final String message) { if (encryptionManager != null) { return encryptionManager.encrypt(message); } return message; } /** * Starts the connection with the lock server * * @param password connection password * @return {@code true} if successful */ public boolean connectToServer(final char[] password) { boolean result = false; // If a password has been specified, create an EncryptionManager if (password != null && password.length > 0) { encryptionManager = new EncryptionManager(password); } final Bootstrap bootstrap = new Bootstrap(); eventLoopGroup = new NioEventLoopGroup(); bootstrap.group(eventLoopGroup).channel(NioSocketChannel.class).handler(new Initializer()) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, ConnectionFactory.getConnectionTimeout() * 1000) .option(ChannelOption.SO_KEEPALIVE, true); try { // Start the connection attempt. channel = bootstrap.connect(host, port).sync().channel(); channel.writeAndFlush(encrypt(UUID_PREFIX + uuid) + EOL_DELIMITER).sync(); // send this channels uuid result = true; logger.info("Connection made with Distributed Lock Server"); } catch (final InterruptedException e) { logger.log(Level.SEVERE, "Failed to connect to Distributed Lock Server", e); disconnectFromServer(); } return result; } /** * Disconnects from the lock server */ public void disconnectFromServer() { try { channel.close().sync(); } catch (InterruptedException e) { logger.log(Level.SEVERE, e.getLocalizedMessage(), e); } executorService.shutdown(); eventLoopGroup.shutdownGracefully(); eventLoopGroup = null; channel = null; logger.info("Disconnected from the Distributed Lock Server"); } @Override public synchronized ReentrantReadWriteLock getLock(final String lockId) { DistributedReadWriteLock lock = lockMap.get(lockId); if (lock == null) { lock = new DistributedReadWriteLock(lockId); lockMap.put(lockId, lock); } return lock; } private CountDownLatch getLatch(final String lockMessage) { latchLock.lock(); try { CountDownLatch semaphore = latchMap.get(lockMessage); if (semaphore == null) { semaphore = new CountDownLatch(1); latchMap.put(lockMessage, semaphore); } return semaphore; } finally { latchLock.unlock(); } } private void lock(final String lockId, final String type) { changeLockState(lockId, type, DistributedLockServer.LOCK); } private void unlock(final String lockId, final String type) { changeLockState(lockId, type, DistributedLockServer.UNLOCK); } @SuppressFBWarnings({ "JLM_JSR166_UTILCONCURRENT_MONITORENTER" }) private void changeLockState(final String lockId, final String type, final String lockState) { final String threadId = uuid + '-' + Thread.currentThread().getId(); final String lockMessage = MessageFormat.format(PATTERN, lockState, lockId, threadId, type); final CountDownLatch responseLatch = getLatch(lockMessage); //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (responseLatch) { // synchronize on the lock to prevent concurrency errors boolean result = false; try { // send the message to the server and wait until it if flushed channel.writeAndFlush(encrypt(lockMessage) + EOL_DELIMITER).sync(); for (int i = 0; i < 2; i++) { result = responseLatch.await(45L, TimeUnit.SECONDS); if (!result) { logger.log(Level.WARNING, "Excessive wait for release of the lock latch for: {0}", lockId); } else { break; } } if (!result) { // check for a failed release or deadlock logger.log(Level.SEVERE, "Failed to release the lock latch for: {0}", lockId); latchLock.lock(); try { responseLatch.countDown(); // force a countdown to occur latchMap.remove(lockId); // force removal } finally { latchLock.unlock(); } } } catch (InterruptedException e) { logger.log(Level.SEVERE, e.getLocalizedMessage(), e); } } } private void processMessage(final String lockMessage) { final String plainMessage; if (encryptionManager != null) { plainMessage = encryptionManager.decrypt(lockMessage); } else { plainMessage = lockMessage; } //logger.info(plainMessage); /** lock_action, lock_id, thread_id, lock_type */ // unlock,account,3456384756384563,read // lock,account,3456384756384563,write latchLock.lock(); try { final CountDownLatch responseLatch = getLatch(plainMessage); responseLatch.countDown(); // this should release the responseLatch allowing a blocked thread to continue latchMap.remove(plainMessage); // remove the used up latch } finally { latchLock.unlock(); } } private class Initializer extends ChannelInitializer<SocketChannel> { @Override public void initChannel(final SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); // Add the text line codec combination first, pipeline.addLast("framer", new DelimiterBasedFrameDecoder(8192, true, Delimiters.lineDelimiter())); pipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8)); pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8)); // and then business logic. pipeline.addLast("handler", new ClientHandler()); } } /** * Handles a client-side channel. */ @ChannelHandler.Sharable private class ClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(final ChannelHandlerContext ctx, final Object msg) { executorService.submit(() -> { processMessage(msg.toString()); ReferenceCountUtil.release(msg); }); } @Override public void exceptionCaught(final ChannelHandlerContext ctx, final Throwable cause) throws Exception { super.exceptionCaught(ctx, cause); logger.log(Level.WARNING, "Unexpected exception from downstream.", cause); ctx.close(); } } private class DistributedReadWriteLock extends ReentrantReadWriteLock { private final String lockId; private final DistributedReadWriteLock.ReadLock readLock; private final DistributedReadWriteLock.WriteLock writeLock; DistributedReadWriteLock(final String lockId) { super(); this.lockId = lockId; readLock = new DistributedReadWriteLock.ReadLock(this); writeLock = new DistributedReadWriteLock.WriteLock(this); } @Override @NotNull public ReentrantReadWriteLock.ReadLock readLock() { return readLock; } @Override @NotNull public ReentrantReadWriteLock.WriteLock writeLock() { return writeLock; } class ReadLock extends ReentrantReadWriteLock.ReadLock { ReadLock(final ReentrantReadWriteLock lock) { super(lock); } @Override public void lock() { DistributedLockManager.this.lock(lockId, DistributedLockServer.LOCK_TYPE_READ); super.lock(); } @Override public void unlock() { DistributedLockManager.this.unlock(lockId, DistributedLockServer.LOCK_TYPE_READ); super.unlock(); } } class WriteLock extends ReentrantReadWriteLock.WriteLock { WriteLock(final ReentrantReadWriteLock lock) { super(lock); } @Override public void lock() { DistributedLockManager.this.lock(lockId, DistributedLockServer.LOCK_TYPE_WRITE); super.lock(); } @Override public void unlock() { DistributedLockManager.this.unlock(lockId, DistributedLockServer.LOCK_TYPE_WRITE); super.unlock(); } } } }