package net.i2p.client.impl;
import java.util.concurrent.atomic.AtomicLong;
import net.i2p.I2PAppContext;
import net.i2p.client.I2PSession;
import net.i2p.client.SendMessageStatusListener;
import net.i2p.data.i2cp.MessageId;
import net.i2p.data.i2cp.MessageStatusMessage;
import net.i2p.util.Log;
/**
* Contains the state of a payload message being sent to a peer.
*
* Originally was a general-purpose waiter.
* Then we got rid of guaranteed delivery.
* Then we stopped waiting for accept in best-effort delivery.
* Brought back to life for asynchronous status delivery to the client.
*/
class MessageState {
private final I2PAppContext _context;
private final Log _log;
private final long _nonce;
private final String _prefix;
private MessageId _id;
private final long _created;
private final long _expires;
private final SendMessageStatusListener _listener;
private final I2PSession _session;
private enum State { INIT, ACCEPTED, PROBABLE_FAIL, FAIL, SUCCESS };
private State _state = State.INIT;
/**
* For synchronous waiting for accept with waitForAccept().
* UNUSED.
*/
public MessageState(I2PAppContext ctx, long nonce, String prefix) {
_context = ctx;
_log = ctx.logManager().getLog(MessageState.class);
_nonce = nonce;
_prefix = prefix + '[' + _nonce + "]: ";
_created = ctx.clock().now();
_expires = _created + 60*1000L;
_listener = null;
_session = null;
}
/**
* For asynchronous notification
* @param expires absolute time (not interval)
* @since 0.9.14
*/
public MessageState(I2PAppContext ctx, long nonce, I2PSession session,
long expires, SendMessageStatusListener listener) {
_context = ctx;
_log = ctx.logManager().getLog(MessageState.class);
_nonce = nonce;
_prefix = session.toString() + " [" + _nonce + "]: ";
_created = ctx.clock().now();
_expires = expires;
_listener = listener;
_session = session;
}
public void receive(int status) {
State oldState;
State newState;
synchronized (this) {
oldState = _state;
locked_update(status);
newState = _state;
this.notifyAll();
}
if (_listener != null) {
// only notify on changing state, and only if we haven't expired
if (oldState != newState && _expires > _context.clock().now())
_listener.messageStatus(_session, _nonce, status);
}
}
public void setMessageId(MessageId id) {
_id = id;
}
public MessageId getMessageId() {
return _id;
}
public long getElapsed() {
return _context.clock().now() - _created;
}
/**
* @since 0.9.14
*/
public long getExpires() {
return _expires;
}
/**
* For guaranteed/best effort only. Not really used.
*/
public void waitForAccept(long expiration) throws InterruptedException {
while (true) {
long timeToWait = expiration - _context.clock().now();
if (timeToWait <= 0) {
if (_log.shouldLog(Log.WARN))
_log.warn(_prefix + "Expired waiting for the status");
return;
}
synchronized (this) {
if (_state != State.INIT) {
if (_log.shouldLog(Log.DEBUG))
_log.debug(_prefix + "Received a confirm (one way or the other)");
return;
}
if (timeToWait > 5000)
timeToWait = 5000;
this.wait(timeToWait);
}
}
}
/**
* Update our flags
* @since 0.9.14
*/
private void locked_update(int status) {
switch (status) {
case MessageStatusMessage.STATUS_SEND_ACCEPTED:
// only trumps init
if (_state == State.INIT)
_state = State.ACCEPTED;
break;
case MessageStatusMessage.STATUS_SEND_BEST_EFFORT_FAILURE:
case MessageStatusMessage.STATUS_SEND_GUARANTEED_FAILURE:
// does not trump failure or success
if (_state != State.FAIL && _state != State.SUCCESS)
_state = State.PROBABLE_FAIL;
break;
case MessageStatusMessage.STATUS_SEND_FAILURE_LOCAL:
case MessageStatusMessage.STATUS_SEND_FAILURE_ROUTER:
case MessageStatusMessage.STATUS_SEND_FAILURE_NETWORK:
case MessageStatusMessage.STATUS_SEND_FAILURE_BAD_SESSION:
case MessageStatusMessage.STATUS_SEND_FAILURE_BAD_MESSAGE:
case MessageStatusMessage.STATUS_SEND_FAILURE_BAD_OPTIONS:
case MessageStatusMessage.STATUS_SEND_FAILURE_OVERFLOW:
case MessageStatusMessage.STATUS_SEND_FAILURE_EXPIRED:
case MessageStatusMessage.STATUS_SEND_FAILURE_LOCAL_LEASESET:
case MessageStatusMessage.STATUS_SEND_FAILURE_NO_TUNNELS:
case MessageStatusMessage.STATUS_SEND_FAILURE_UNSUPPORTED_ENCRYPTION:
case MessageStatusMessage.STATUS_SEND_FAILURE_DESTINATION:
case MessageStatusMessage.STATUS_SEND_FAILURE_BAD_LEASESET:
case MessageStatusMessage.STATUS_SEND_FAILURE_EXPIRED_LEASESET:
case MessageStatusMessage.STATUS_SEND_FAILURE_NO_LEASESET:
case SendMessageStatusListener.STATUS_CANCELLED:
// does not trump success
if (_state != State.SUCCESS)
_state = State.FAIL;
break;
case MessageStatusMessage.STATUS_SEND_BEST_EFFORT_SUCCESS:
case MessageStatusMessage.STATUS_SEND_GUARANTEED_SUCCESS:
case MessageStatusMessage.STATUS_SEND_SUCCESS_LOCAL:
// trumps all
_state = State.SUCCESS;
break;
default:
break;
}
}
/**
* @return true if accepted (fixme and not failed)
* @since 0.9.14
*/
public boolean wasAccepted() {
synchronized (this) {
return _state != State.INIT && _state != State.FAIL;
}
}
/**
* @return true if successful
* @since 0.9.14
*/
public boolean wasSuccessful() {
synchronized (this) {
return _state == State.SUCCESS;
}
}
public void cancel() {
// Inject a fake status
receive(SendMessageStatusListener.STATUS_CANCELLED);
}
}