/**
* Copyright (c) 2011 Yahoo! Inc. All rights reserved.
*
* Licensed 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. See accompanying LICENSE file.
*/
package com.yahoo.omid.client;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executors;
import org.apache.hadoop.conf.Configuration;
import org.jboss.netty.bootstrap.ClientBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
import org.jboss.netty.channel.socket.nio.NioClientSocketChannelFactory;
import org.jboss.netty.handler.execution.ExecutionHandler;
import org.jboss.netty.handler.execution.OrderedMemoryAwareThreadPoolExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.yahoo.omid.replication.Zipper;
import com.yahoo.omid.replication.ZipperState;
import com.yahoo.omid.tso.Committed;
import com.yahoo.omid.tso.RowKey;
import com.yahoo.omid.tso.TSOMessage;
import com.yahoo.omid.tso.messages.AbortRequest;
import com.yahoo.omid.tso.messages.AbortedTransactionReport;
import com.yahoo.omid.tso.messages.CleanedTransactionReport;
import com.yahoo.omid.tso.messages.CommitQueryRequest;
import com.yahoo.omid.tso.messages.CommitQueryResponse;
import com.yahoo.omid.tso.messages.CommitRequest;
import com.yahoo.omid.tso.messages.CommitResponse;
import com.yahoo.omid.tso.messages.CommittedTransactionReport;
import com.yahoo.omid.tso.messages.FullAbortRequest;
import com.yahoo.omid.tso.messages.LargestDeletedTimestampReport;
import com.yahoo.omid.tso.messages.TimestampRequest;
import com.yahoo.omid.tso.messages.TimestampResponse;
import com.yahoo.omid.tso.serialization.TSODecoder;
import com.yahoo.omid.tso.serialization.TSOEncoder;
public class TSOClient extends SimpleChannelHandler {
private static final Logger LOG = LoggerFactory.getLogger(TSOClient.class);
public static long askedTSO = 0;
public enum Result {
OK, ABORTED
};
private Queue<CreateCallback> createCallbacks;
private Map<Long, CommitCallback> commitCallbacks;
private Map<Long, List<CommitQueryCallback>> isCommittedCallbacks;
private Committed committed = new Committed();
private Set<Long> aborted = Collections.synchronizedSet(new HashSet<Long>(1000));
private long largestDeletedTimestamp;
private long connectionTimestamp = 0;
private boolean hasConnectionTimestamp = false;
private ChannelFactory factory;
private ClientBootstrap bootstrap;
private Channel channel;
private InetSocketAddress addr;
private int max_retries;
private int retries;
private int retry_delay_ms;
private Timer retryTimer;
private enum State {
DISCONNECTED, CONNECTING, CONNECTED, RETRY_CONNECT_WAIT
};
private interface Op { //有5个实现类
public void execute(Channel channel);
public void error(Exception e);
}
private class AbortOp implements Op {
long transactionId;
AbortOp(long transactionid) throws IOException {
this.transactionId = transactionid;
}
public void execute(Channel channel) {
try {
synchronized (commitCallbacks) {
if (commitCallbacks.containsKey(transactionId)) {
throw new IOException("Already committing transaction " + transactionId);
}
}
AbortRequest ar = new AbortRequest();
ar.startTimestamp = transactionId;
ChannelFuture f = channel.write(ar);
f.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
if (!future.isSuccess()) {
error(new IOException("Error writing to socket"));
}
}
});
} catch (Exception e) {
error(e);
}
}
public void error(Exception e) {
}
}
private class NewTimestampOp implements Op {
private CreateCallback cb;
NewTimestampOp(CreateCallback cb) {
this.cb = cb;
}
public void execute(Channel channel) {
try {
synchronized (createCallbacks) {
createCallbacks.add(cb);
}
TimestampRequest tr = new TimestampRequest();
ChannelFuture f = channel.write(tr);
f.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
if (!future.isSuccess()) {
error(new IOException("Error writing to socket"));
}
}
});
} catch (Exception e) {
error(e);
}
}
public void error(Exception e) {
synchronized (createCallbacks) {
createCallbacks.remove();
}
cb.error(e);
}
}
private class CommitQueryOp implements Op {
long startTimestamp;
long pendingWriteTimestamp;
CommitQueryCallback cb;
CommitQueryOp(long startTimestamp, long pendingWriteTimestamp, CommitQueryCallback cb) {
this.startTimestamp = startTimestamp;
this.pendingWriteTimestamp = pendingWriteTimestamp;
this.cb = cb;
}
public void execute(Channel channel) {
try {
synchronized (isCommittedCallbacks) {
List<CommitQueryCallback> callbacks = isCommittedCallbacks.get(startTimestamp);
if (callbacks == null) {
callbacks = new ArrayList<CommitQueryCallback>(1);
}
callbacks.add(cb);
isCommittedCallbacks.put(startTimestamp, callbacks);
}
CommitQueryRequest qr = new CommitQueryRequest(startTimestamp, pendingWriteTimestamp);
ChannelFuture f = channel.write(qr);
f.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
if (!future.isSuccess()) {
error(new IOException("Error writing to socket"));
}
}
});
} catch (Exception e) {
error(e);
}
}
public void error(Exception e) {
synchronized (isCommittedCallbacks) {
isCommittedCallbacks.remove(startTimestamp);
}
cb.error(e);
}
}
private class CommitOp implements Op {
long transactionId;
RowKey[] rows;
CommitCallback cb;
CommitOp(long transactionid, RowKey[] rows, CommitCallback cb) throws IOException {
this.transactionId = transactionid;
this.rows = rows;
this.cb = cb;
}
public void execute(Channel channel) {
try {
synchronized (commitCallbacks) {
if (commitCallbacks.containsKey(transactionId)) {
throw new IOException("Already committing transaction " + transactionId);
}
commitCallbacks.put(transactionId, cb); //在收到CommitResponse后删除
}
CommitRequest cr = new CommitRequest();
cr.startTimestamp = transactionId;
cr.rows = rows;
ChannelFuture f = channel.write(cr);
f.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
if (!future.isSuccess()) {
error(new IOException("Error writing to socket"));
}
}
});
} catch (Exception e) {
error(e);
}
}
public void error(Exception e) {
synchronized (commitCallbacks) {
commitCallbacks.remove(transactionId);
}
cb.error(e);
}
}
private class AbortCompleteOp implements Op {
long transactionId;
AbortCompleteCallback cb;
AbortCompleteOp(long transactionId, AbortCompleteCallback cb) throws IOException {
this.transactionId = transactionId;
this.cb = cb;
}
public void execute(Channel channel) {
try {
FullAbortRequest far = new FullAbortRequest();
far.startTimestamp = transactionId;
ChannelFuture f = channel.write(far);
f.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
if (!future.isSuccess()) {
error(new IOException("Error writing to socket"));
} else {
cb.complete();
}
}
});
} catch (Exception e) {
error(e);
}
}
public void error(Exception e) {
cb.error(e);
}
}
private ArrayBlockingQueue<Op> queuedOps;
private State state;
public TSOClient(Configuration conf) throws IOException {
state = State.DISCONNECTED;
queuedOps = new ArrayBlockingQueue<Op>(200);
retryTimer = new Timer(true);
commitCallbacks = Collections.synchronizedMap(new HashMap<Long, CommitCallback>());
isCommittedCallbacks = Collections.synchronizedMap(new HashMap<Long, List<CommitQueryCallback>>());
createCallbacks = new ConcurrentLinkedQueue<CreateCallback>();
channel = null;
System.out.println("Starting TSOClient");
// Start client with Nb of active threads = 3 as maximum.
factory = new NioClientSocketChannelFactory(Executors.newCachedThreadPool(), Executors.newCachedThreadPool(), 3);
// Create the bootstrap
bootstrap = new ClientBootstrap(factory);
int executorThreads = conf.getInt("tso.executor.threads", 3);
bootstrap.getPipeline().addLast("executor",
new ExecutionHandler(new OrderedMemoryAwareThreadPoolExecutor(executorThreads, 1024 * 1024, 4 * 1024 * 1024)));
bootstrap.getPipeline().addLast("handler", this);
bootstrap.setOption("tcpNoDelay", false);
bootstrap.setOption("keepAlive", true);
bootstrap.setOption("reuseAddress", true);
bootstrap.setOption("connectTimeoutMillis", 100);
String host = conf.get("tso.host");
int port = conf.getInt("tso.port", 1234);
max_retries = conf.getInt("tso.max_retries", 100);
retry_delay_ms = conf.getInt("tso.retry_delay_ms", 1000);
if (host == null) {
throw new IOException("tso.host missing from configuration");
}
addr = new InetSocketAddress(host, port);
connectIfNeeded();
}
private State connectIfNeeded() throws IOException {
synchronized (state) {
if (state == State.CONNECTED || state == State.CONNECTING) {
return state;
}
if (state == State.RETRY_CONNECT_WAIT) {
return State.CONNECTING;
}
if (retries > max_retries) {
IOException e = new IOException("Max connection retries exceeded");
bailout(e);
throw e;
}
retries++;
bootstrap.connect(addr).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
LOG.debug("Connection completed. Success: " + future.isSuccess());
}
});
state = State.CONNECTING;
return state;
}
}
private void withConnection(Op op) throws IOException {
State state = connectIfNeeded();
if (state == State.CONNECTING) {
try {
queuedOps.put(op);
} catch (InterruptedException e) {
throw new IOException("Couldn't add new operation", e);
}
} else if (state == State.CONNECTED) {
op.execute(channel);
} else {
throw new IOException("Invalid connection state " + state);
}
}
public void getNewTimestamp(CreateCallback cb) throws IOException {
withConnection(new NewTimestampOp(cb));
}
public void isCommitted(long startTimestamp, long pendingWriteTimestamp, CommitQueryCallback cb) throws IOException {
withConnection(new CommitQueryOp(startTimestamp, pendingWriteTimestamp, cb));
}
public void abort(long transactionId) throws IOException {
withConnection(new AbortOp(transactionId));
}
public void commit(long transactionId, RowKey[] rows, CommitCallback cb) throws IOException {
withConnection(new CommitOp(transactionId, rows, cb));
}
public void completeAbort(long transactionId, AbortCompleteCallback cb) throws IOException {
withConnection(new AbortCompleteOp(transactionId, cb));
}
@Override
synchronized public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) {
e.getChannel().getPipeline().addFirst("decoder", new TSODecoder(new Zipper()));
e.getChannel().getPipeline().addAfter("decoder", "encoder", new TSOEncoder());
}
/**
* Starts the traffic
*/
@Override
synchronized public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) {
synchronized (state) {
channel = e.getChannel();
state = State.CONNECTED;
retries = 0;
}
clearState();
LOG.debug("Channel connected");
Op o = queuedOps.poll();
while (o != null && state == State.CONNECTED) {
o.execute(channel);
o = queuedOps.poll();
}
}
private void clearState() {
committed = new Committed();
aborted.clear();
largestDeletedTimestamp = 0;
connectionTimestamp = 0;
hasConnectionTimestamp = false;
}
@Override
synchronized public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
synchronized (state) {
LOG.debug("Channel disconnected");
channel = null;
state = State.DISCONNECTED;
for (CreateCallback cb : createCallbacks) {
cb.error(new IOException("Channel Disconnected"));
}
for (CommitCallback cb : commitCallbacks.values()) {
cb.error(new IOException("Channel Disconnected"));
}
for (List<CommitQueryCallback> lcqb : isCommittedCallbacks.values()) {
for (CommitQueryCallback cqb : lcqb) {
cqb.error(new IOException("Channel Disconnected"));
}
}
createCallbacks.clear();
commitCallbacks.clear();
isCommittedCallbacks.clear();
connectIfNeeded();
}
}
public boolean validRead(long transaction, long startTimestamp) throws IOException {
if (transaction == startTimestamp)
return true;
if (aborted.contains(transaction))
return false;
long commitTimestamp = committed.getCommit(transaction);
if (commitTimestamp != -1)
return commitTimestamp <= startTimestamp;
if (hasConnectionTimestamp && transaction > connectionTimestamp)
return transaction <= largestDeletedTimestamp;
if (transaction <= largestDeletedTimestamp)
return true;
askedTSO++;
SyncCommitQueryCallback cb = new SyncCommitQueryCallback();
isCommitted(startTimestamp, transaction, cb);
try {
cb.await();
} catch (InterruptedException e) {
throw new IOException("Commit query didn't complete", e);
}
return cb.isCommitted();
}
/**
* When a message is received, handle it based on its type
*/
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
if (LOG.isTraceEnabled()) {
LOG.trace("messageReceived " + e.getMessage());
}
Object msg = e.getMessage();
if (msg instanceof CommitResponse) {
CommitResponse r = (CommitResponse) msg;
CommitCallback cb = null;
synchronized (commitCallbacks) {
cb = commitCallbacks.remove(r.startTimestamp);
}
if (cb == null) {
LOG.error("Received a commit response for a nonexisting commit");
return;
}
cb.complete(r.committed ? Result.OK : Result.ABORTED, r.commitTimestamp);
} else if (msg instanceof TimestampResponse) {
CreateCallback cb = createCallbacks.poll();
long timestamp = ((TimestampResponse) msg).timestamp;
if (!hasConnectionTimestamp || timestamp < connectionTimestamp) {
hasConnectionTimestamp = true;
connectionTimestamp = timestamp;
}
if (cb == null) {
LOG.error("Receiving a timestamp response, but none requested: " + timestamp);
return;
}
cb.complete(timestamp);
} else if (msg instanceof CommitQueryResponse) {
CommitQueryResponse r = (CommitQueryResponse) msg;
if (r.commitTimestamp != 0) {
committed.commit(r.queryTimestamp, r.commitTimestamp);
} else if (r.committed) {
committed.commit(r.queryTimestamp, largestDeletedTimestamp);
}
List<CommitQueryCallback> cbs = null;
synchronized (isCommittedCallbacks) {
cbs = isCommittedCallbacks.remove(r.startTimestamp);
}
if (cbs == null) {
LOG.error("Received a commit query response for a nonexisting request");
return;
}
for (CommitQueryCallback cb : cbs) {
cb.complete(r.committed);
}
} else if (msg instanceof CommittedTransactionReport) {
CommittedTransactionReport ctr = (CommittedTransactionReport) msg;
committed.commit(ctr.startTimestamp, ctr.commitTimestamp);
} else if (msg instanceof CleanedTransactionReport) {
CleanedTransactionReport r = (CleanedTransactionReport) msg;
aborted.remove(r.startTimestamp);
} else if (msg instanceof AbortedTransactionReport) {
AbortedTransactionReport r = (AbortedTransactionReport) msg;
aborted.add(r.startTimestamp);
} else if (msg instanceof LargestDeletedTimestampReport) {
LargestDeletedTimestampReport r = (LargestDeletedTimestampReport) msg;
largestDeletedTimestamp = r.largestDeletedTimestamp;
committed.raiseLargestDeletedTransaction(r.largestDeletedTimestamp);
} else if (msg instanceof ZipperState) {
// ignore
} else {
LOG.error("Unknown message received " + msg);
}
processMessage((TSOMessage) msg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
LOG.error("Unexpected exception", e.getCause());
synchronized (state) {
if (state == State.CONNECTING) {
state = State.RETRY_CONNECT_WAIT;
if (LOG.isDebugEnabled()) {
LOG.debug("Retrying connect in " + retry_delay_ms + "ms " + retries);
}
try {
retryTimer.schedule(new TimerTask() {
public void run() {
synchronized (state) {
state = State.DISCONNECTED;
try {
connectIfNeeded();
} catch (IOException e) {
bailout(e);
}
}
}
}, retry_delay_ms);
} catch (Exception cause) {
bailout(cause);
}
} else {
LOG.error("Exception on channel", e.getCause());
}
}
}
public void bailout(Exception cause) {
synchronized (state) {
state = State.DISCONNECTED;
}
LOG.error("Unrecoverable error in client, bailing out", cause);
Exception e = new IOException("Unrecoverable error", cause);
Op o = queuedOps.poll();
;
while (o != null) {
o.error(e);
o = queuedOps.poll();
}
synchronized (createCallbacks) {
for (CreateCallback cb : createCallbacks) {
cb.error(e);
}
createCallbacks.clear();
}
synchronized (commitCallbacks) {
for (CommitCallback cb : commitCallbacks.values()) {
cb.error(e);
}
commitCallbacks.clear();
}
synchronized (isCommittedCallbacks) {
for (List<CommitQueryCallback> cbs : isCommittedCallbacks.values()) {
for (CommitQueryCallback cb : cbs) {
cb.error(e);
}
}
isCommittedCallbacks.clear();
}
}
protected void processMessage(TSOMessage msg) {
}
public void close() {
bootstrap.releaseExternalResources();
}
}