/* ************************************************************************
#
# DivConq
#
# http://divconq.com/
#
# Copyright:
# Copyright 2014 eTimeline, LLC. All rights reserved.
#
# License:
# See the license.txt file in the project's top-level directory for details.
#
# Authors:
# * Andy White
#
************************************************************************ */
package divconq.session;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import divconq.bus.MessageUtil;
import divconq.bus.net.StreamMessage;
import divconq.filestore.CommonPath;
import divconq.hub.Hub;
import divconq.lang.op.OperationContext;
import divconq.lang.op.OperationResult;
import divconq.log.Logger;
import divconq.struct.RecordStruct;
import divconq.struct.Struct;
/**
* TODO track if we are a source or dest stream - if a send of "Block" is tried on a dest then error
* if a receive of "Block" on source happens then error
*/
public class DataStreamChannel extends OperationResult {
protected String id = Session.nextUUId();
protected String title = null;
protected CommonPath path = null;
protected String mime = null;
protected Struct params = null;
protected IStreamDriver driver = null;
protected String sessid = null;
protected long timeout = 60 * 1000; // timeout in 1 full minute of no activity on the channel
protected long deadline = 0;
protected boolean closed = false;
protected long started = System.currentTimeMillis();
protected final Lock completionlock = new ReentrantLock();
protected boolean completed = false;
// contains the FromHub, Session, Channel info to reply to
protected RecordStruct binding = null;
public String getId() {
return this.id;
}
public String getTitle() {
return this.title;
}
public void setTitle(String v) {
this.title = v;
}
public String getMime() {
return this.mime;
}
public void setMime(String v) {
this.mime = v;
}
public CommonPath getPath() {
return this.path;
}
public void setPath(CommonPath v) {
this.path = v;
}
public void setPath(String v) {
this.path = new CommonPath(v);
}
public IStreamDriver getDriver() {
return this.driver;
}
public void setDriver(IStreamDriver v) {
this.driver = v;
}
public Struct getParams() {
return this.params;
}
public void setParams(Struct v) {
this.params = v;
}
// in seconds
public int getTimeout() {
return (int) (this.timeout / 1000);
}
// set in seconds
public void setTimeout(int v) {
this.timeout = v * 1000;
}
// in seconds
public int getDeadline() {
return (int) (this.deadline / 1000);
}
// set in seconds
public void setDeadline(int v) {
this.deadline = v * 1000;
}
public RecordStruct getBinding() {
return this.binding;
}
public void setBinding(RecordStruct v) {
this.binding = v;
if (!v.isFieldEmpty("FilePath"))
this.setPath(v.getFieldAsString("FilePath"));
if (!v.isFieldEmpty("Mime"))
this.setMime(v.getFieldAsString("Mime"));
}
public String getSessionId() {
return this.sessid;
}
public DataStreamChannel(String sessid, String title) {
super();
this.title = title;
this.sessid = sessid;
}
public DataStreamChannel(String sessid, String title, RecordStruct binding) {
super();
this.title = title;
this.sessid = sessid;
this.binding = binding;
}
public void resume() {
OperationContext.set(this.opcontext);
}
/**
* abort is only for our end of the channel. if the other end sends an error message
* we should just close, not abort. don't send messages to a channel that has errored
*
*/
public void abort() {
if (Logger.isDebug())
Logger.debug("Data stream aborted: " + this.id);
this.send(MessageUtil.streamError(1, "Aborting data stream: " + this));
this.close();
}
public void close() {
if (Logger.isDebug())
Logger.debug("Data stream closed: " + this.id);
this.completionlock.lock();
try {
if (this.completed)
return;
OperationContext.set(this.opcontext);
// collect inactive before error logging, logging updates the activity
boolean inactive = this.isInactive();
this.errorTr(196, this);
if (this.isOverdue())
this.errorTr(222, this);
else if (inactive)
this.errorTr(223, this);
this.closed = true;
if (this.driver != null)
this.driver.cancel();
// this.send(MessageUtil.streamError(1, "Connection killed: " + this));
this.complete();
}
finally {
this.completionlock.unlock();
}
}
public boolean isClosed() {
return this.closed;
}
// must report if timed out, even if completed - otherwise Worker thread might lock forever if WorkBucket kills us first
public boolean isHung() {
return this.isInactive() || this.isOverdue();
}
public boolean isInactive() {
// has activity been quiet for longer than timeout?
if ((this.timeout > 0) && (this.getLastActivity() < (System.currentTimeMillis() - this.timeout))) {
if (Logger.isDebug())
Logger.debug("Data stream reports being inactive: " + this.id);
return true;
}
return false;
}
public boolean isOverdue() {
// has activity been working too long?
if ((this.deadline > 0) && (this.started < (System.currentTimeMillis() - this.deadline))) {
if (Logger.isDebug())
Logger.debug("Data stream reports being overdue: " + this.id);
return true;
}
return false;
}
public void complete() {
if (Logger.isDebug())
Logger.debug("Data stream has completed: " + this.id);
// make sure we complete in the correct context (only worker should call this method)
OperationContext.set(this.opcontext);
this.completionlock.lock();
try {
// don't complete twice
if (this.completed)
return;
this.completed = true;
this.traceTr(224, this.getCode());
Logger.info("Channel completed: " + this);
// TODO make sure channel gets cleared
Hub.instance.getSessions().lookup(this.sessid).removeChannel(this.id);
}
finally {
this.completionlock.unlock();
}
}
@Override
public String toString() {
return this.getTitle() + " (" + this.getId() + ")";
}
public boolean isComplete() {
return this.completed;
}
public RecordStruct toStatusReport() {
RecordStruct rec = new RecordStruct();
rec.setField("Id", this.id);
rec.setField("Title", this.title);
rec.setField("Canceled", this.closed);
rec.setField("Completed", this.completed);
// TODO anything else?
return rec;
}
public void deliverMessage(StreamMessage msg) {
this.touch();
OperationContext.set(this.opcontext);
if (Logger.isDebug())
Logger.debug("Data Stream Message arrived: " + msg.toPrettyString());
if (msg.hasErrors()) {
this.close();
msg.release();
return;
}
OperationResult vres = msg.validate("StreamMessage");
if (vres.hasErrors()) {
this.abort();
msg.release();
return;
}
if (this.isClosed()) {
this.send(MessageUtil.streamError(1, "Channel is closed!"));
msg.release();
return;
}
if (this.driver != null)
this.driver.message(msg);
}
public OperationResult send(StreamMessage msg) {
OperationContext.set(this.opcontext);
if (Logger.isDebug())
Logger.debug("Data Stream Message sending: " + msg.toPrettyString());
msg.setField("FromHub", OperationContext.getHubId());
msg.setField("FromSession", this.sessid);
msg.setField("FromChannel", this.id);
if (this.binding == null)
return new OperationResult();
return Hub.instance.getBus().sendReply(msg, this.binding);
}
}