package org.yamcs.archive; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yamcs.YamcsException; import org.yamcs.api.YamcsApiException; import org.yamcs.protobuf.Yamcs.EndAction; import org.yamcs.protobuf.Yamcs.ProtoDataType; import org.yamcs.protobuf.Yamcs.ReplayRequest; import org.yamcs.protobuf.Yamcs.ReplaySpeed; import org.yamcs.protobuf.Yamcs.ReplaySpeed.ReplaySpeedType; import org.yamcs.protobuf.Yamcs.ReplayStatus; import org.yamcs.protobuf.Yamcs.ReplayStatus.ReplayState; import org.yamcs.security.AuthenticationToken; import org.yamcs.utils.TimeEncoding; import org.yamcs.xtce.XtceDb; import org.yamcs.yarch.SpeedLimitStream; import org.yamcs.yarch.SpeedSpec; import org.yamcs.yarch.Stream; import org.yamcs.yarch.StreamSubscriber; import org.yamcs.yarch.Tuple; import org.yamcs.yarch.YarchDatabase; import org.yamcs.yarch.streamsql.ParseException; import org.yamcs.yarch.streamsql.StreamSqlException; import com.google.protobuf.MessageLite; /** * Performs a replay from Yarch So far supported are: TM packets, PP groups, Events, Parameters and Command History. * * It relies on handlers for each data type. * Each handler creates a stream, the streams are merged and the output is sent to the listener * This class can also handle * pause/resume: simply stop sending data * seek: closes the streams and creates new ones with a different starting time. * * @author nm * */ public class YarchReplay implements StreamSubscriber { ReplayServer replayServer; volatile String streamName; volatile boolean quitting=false; private volatile ReplayState state = ReplayState.INITIALIZATION; static Logger log=LoggerFactory.getLogger(YarchReplay.class.getName()); private volatile String errorString=""; int numPacketsSent; final String instance; static AtomicInteger counter=new AtomicInteger(); XtceDb xtceDb; volatile ReplayRequest currentRequest; Map<ProtoDataType,ReplayHandler> handlers; private Semaphore pausedSemaphore=new Semaphore(0); boolean dropTuple=false; //set to true when jumping to a different time volatile boolean ignoreClose; ReplayListener listener; public YarchReplay(ReplayServer replayServer, ReplayRequest rr, ReplayListener listener, XtceDb xtceDb, AuthenticationToken authToken) throws IOException, YamcsException, YamcsApiException { this.listener = listener; this.replayServer=replayServer; this.xtceDb=xtceDb; this.instance=replayServer.instance; if (!rr.hasPacketRequest() && !rr.hasParameterRequest() && !rr.hasEventRequest() && !rr.hasPpRequest() && !rr.hasCommandHistoryRequest()) { throw new YamcsException("Empty replay request"); } setRequest(rr, authToken); } public void setRequest(ReplayRequest newRequest, AuthenticationToken authToken) throws YamcsException { if(state!=ReplayState.INITIALIZATION && state!=ReplayState.STOPPED) { throw new YamcsException("changing the request only supported in the INITIALIZATION and STOPPED states"); } //get the start/stop from utcStart/utcStop ReplayRequest.Builder b = ReplayRequest.newBuilder(newRequest); if(!newRequest.hasStart() && newRequest.hasUtcStart()) { b.setStart(TimeEncoding.parse(newRequest.getUtcStart())); } if(!newRequest.hasStop() && newRequest.hasUtcStop()) { b.setStop(TimeEncoding.parse(newRequest.getUtcStop())); } newRequest = b.build(); log.debug("Replay request for time: [{}, {}]", (newRequest.hasStart() ? TimeEncoding.toString(newRequest.getStart()) : null), (newRequest.hasStop() ? TimeEncoding.toString(newRequest.getStop()) : null)); if (newRequest.hasStart() && newRequest.hasStop() && newRequest.getStart()>newRequest.getStop()) { log.warn("throwing new packetexception: stop time has to be greater than start time"); throw new YamcsException("stop has to be greater than start"); } currentRequest = newRequest; handlers = new HashMap<>(); if (currentRequest.hasParameterRequest()) { throw new YamcsException("The replay cannot handle directly parameters. Please create a replay processor for that"); } if (currentRequest.hasEventRequest()) handlers.put(ProtoDataType.EVENT, new EventReplayHandler()); if (currentRequest.hasPacketRequest()) handlers.put(ProtoDataType.TM_PACKET, new XtceTmReplayHandler(xtceDb)); if (currentRequest.hasPpRequest()) handlers.put(ProtoDataType.PP, new ParameterReplayHandler(xtceDb)); if (currentRequest.hasCommandHistoryRequest()) handlers.put(ProtoDataType.CMD_HISTORY, new CommandHistoryReplayHandler(instance)); for(ReplayHandler rh:handlers.values()) { rh.setRequest(newRequest); } } public ReplayState getState() { return state; } public synchronized void start() { switch(state) { case RUNNING: log.warn("start called when already running, call ignored"); return; case INITIALIZATION: case STOPPED: try { initReplay(); state=ReplayState.RUNNING; } catch (Exception e) { log.error("Got exception when creating the stream: ", e); errorString=e.toString(); state=ReplayState.ERROR; } break; case PAUSED: state=ReplayState.RUNNING; pausedSemaphore.release(); break; case ERROR: case CLOSED: //do nothing? } } private void initReplay() throws StreamSqlException, ParseException { streamName="replay_stream"+counter.incrementAndGet(); StringBuilder sb=new StringBuilder(); sb.append("CREATE STREAM "+streamName+" AS "); if(handlers.size()>1){ sb.append("MERGE "); } boolean first=true; for(ReplayHandler rh:handlers.values()) { String selectCmd=rh.getSelectCmd(); if(selectCmd!=null) { if(first) { first=false; } else sb.append(", "); if(handlers.size()>1) { sb.append("("); } sb.append(selectCmd); if(handlers.size()>1) { sb.append(")"); } } } if(first) { if(currentRequest.getEndAction()==EndAction.QUIT) { signalStateChange(); } return; } if(handlers.size()>1){ sb.append(" USING gentime"); } ReplaySpeed rs; if(currentRequest.hasSpeed()) { rs = currentRequest.getSpeed(); } else { rs = ReplaySpeed.newBuilder().setType(ReplaySpeedType.REALTIME).setParam(1).build(); } switch(rs.getType()) { case AFAP: sb.append(" SPEED AFAP"); break; case FIXED_DELAY: sb.append(" SPEED FIXED_DELAY "+(long)rs.getParam()); break; case REALTIME: sb.append(" SPEED ORIGINAL gentime,"+(long)rs.getParam()); } if(handlers.size()>1 && currentRequest.hasReverse() && currentRequest.getReverse()) { sb.append(" ORDER DESC"); } String query=sb.toString(); log.debug("running query {}", query); YarchDatabase ydb=YarchDatabase.getInstance(instance); ydb.execute(query); Stream s=ydb.getStream(streamName); s.addSubscriber(this); numPacketsSent=0; s.start(); } public void seek(long newReplayTime) throws YamcsException { if(state!=ReplayState.INITIALIZATION) { if(state==ReplayState.PAUSED) { dropTuple=true; pausedSemaphore.release(); } state=ReplayState.INITIALIZATION; String query="CLOSE STREAM "+streamName; ignoreClose=true; try { YarchDatabase db=YarchDatabase.getInstance(instance); if(db.getStream(streamName)!=null) { log.debug("running query: {}", query); db.execute(query); } else { log.debug("Stream already closed"); } } catch (Exception e) { log.error("Got exception when closing the stream: ", e); errorString=e.toString(); state=ReplayState.ERROR; signalStateChange(); } } currentRequest = ReplayRequest.newBuilder(currentRequest).setStart(newReplayTime).build(); for(ReplayHandler rh:handlers.values()) { rh.setRequest(currentRequest); } start(); } public void changeSpeed(ReplaySpeed newSpeed) { log.debug("Changing speed to {}", newSpeed); YarchDatabase ydb=YarchDatabase.getInstance(instance); Stream s=ydb.getStream(streamName); if(!(s instanceof SpeedLimitStream)) { throw new IllegalStateException("Cannot change speed on a "+s.getClass()+" stream"); } else { ((SpeedLimitStream)s).setSpeedSpec(toSpeedSpec(newSpeed)); } ReplayRequest.Builder b = ReplayRequest.newBuilder(currentRequest); b.setSpeed(newSpeed); currentRequest = b.build(); } private SpeedSpec toSpeedSpec(ReplaySpeed speed) { SpeedSpec ss; switch(speed.getType()) { case AFAP: ss=new SpeedSpec(SpeedSpec.Type.AFAP); break; case FIXED_DELAY: ss=new SpeedSpec(SpeedSpec.Type.FIXED_DELAY, (int) speed.getParam()); break; case REALTIME: ss=new SpeedSpec(SpeedSpec.Type.ORIGINAL, "gentime", speed.getParam()); break; default: throw new IllegalArgumentException("Unkown speed type "+speed.getType()); } return ss; } public void pause() { state=ReplayState.PAUSED; } public synchronized void quit() { if(quitting) { return; } quitting=true; log.debug("Replay quitting"); this.notify(); try { YarchDatabase db=YarchDatabase.getInstance(instance); if(db.getStream(streamName)!=null){ db.execute("close stream "+streamName); } } catch (Exception e) { log.error( "Exception whilst quitting", e ); } replayServer.replayFinished(); } @Override public void onTuple(Stream s, Tuple t) { if(quitting) { return; } try { while(state==ReplayState.PAUSED) { pausedSemaphore.acquire(); } if(dropTuple) { dropTuple=false; return; } ProtoDataType type=ProtoDataType.valueOf((Integer)t.getColumn(0)); Object data = handlers.get(type).transform(t); if(data!=null) { listener.newData(type, data); } } catch (Exception e) { if(!quitting) { log.warn("Exception received: ", e); quit(); } } } @Override public synchronized void streamClosed(Stream stream) { for(ReplayHandler rh:handlers.values()) { rh.reset(); } if(ignoreClose) { //this happens when we close the stream to reopen another one ignoreClose=false; return; } if(currentRequest.getEndAction()==EndAction.QUIT) { state=ReplayState.CLOSED; signalStateChange(); quit(); } else if(currentRequest.getEndAction()==EndAction.STOP) { state=ReplayState.STOPPED; signalStateChange(); } else if(currentRequest.getEndAction()==EndAction.LOOP) { if(numPacketsSent==0) { state=ReplayState.STOPPED; //there is no data in this stream signalStateChange(); } else { state=ReplayState.INITIALIZATION; start(); } } } private void signalStateChange() { try { if(quitting) { return; } ReplayStatus.Builder rsb=ReplayStatus.newBuilder().setState(state); if(state==ReplayState.ERROR) { rsb.setErrorMessage(errorString); } ReplayStatus rs=rsb.build(); listener.stateChanged(rs); } catch (Exception e) { log.warn("got exception while signaling the state change: ", e); } } public ReplayRequest getCurrentReplayRequest() { return currentRequest; } }