package org.yamcs.commanding; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.yamcs.ConfigurationException; import org.yamcs.GuardedBy; import org.yamcs.InvalidIdentification; import org.yamcs.ThreadSafe; import org.yamcs.YConfiguration; import org.yamcs.Processor; import org.yamcs.cmdhistory.CommandHistoryPublisher; import org.yamcs.parameter.ParameterConsumer; import org.yamcs.parameter.ParameterRequestManagerImpl; import org.yamcs.parameter.ParameterValue; import org.yamcs.parameter.ParameterValueList; import org.yamcs.parameter.SystemParametersCollector; import org.yamcs.parameter.SystemParametersProducer; import org.yamcs.protobuf.Commanding.CommandId; import org.yamcs.protobuf.Commanding.QueueState; import org.yamcs.protobuf.Pvalue; import org.yamcs.security.AuthenticationToken; import org.yamcs.security.InvalidAuthenticationToken; import org.yamcs.security.Privilege; import org.yamcs.utils.LoggingUtils; import org.yamcs.xtce.CriteriaEvaluator; import org.yamcs.xtce.MatchCriteria; import org.yamcs.xtce.MetaCommand; import org.yamcs.xtce.Parameter; import org.yamcs.xtce.TransmissionConstraint; import org.yamcs.xtce.XtceDb; import org.yamcs.xtceproc.CriteriaEvaluatorImpl; import com.google.common.util.concurrent.AbstractService; /** * @author nm * Implements the management of the control queues for one processor: * - for each command that is sent, based on the sender it finds the queue where the command should go * - depending on the queue state the command can be immediately sent, stored in the queue or rejected * - when the command is immediately sent or rejected, the command queue monitor is not notified * - if the command has transmissionConstraints with timeout > 0, the command can sit in the queue even if the queue is not blocked * * Note: the update of the command monitors is done in the same thread. That means that if the connection to one * of the monitors is lost, there may be a delay of a few seconds. As the monitoring clients will be priviledged users * most likely connected in the same LAN, I don't consider this to be an issue. */ @ThreadSafe public class CommandQueueManager extends AbstractService implements ParameterConsumer, SystemParametersProducer { @GuardedBy("this") private HashMap<String, CommandQueue> queues = new HashMap<>(); CommandReleaser commandReleaser; CommandHistoryPublisher commandHistoryListener; CommandingManager commandingManager; ConcurrentLinkedQueue<CommandQueueListener> monitoringClients = new ConcurrentLinkedQueue<>(); private final Logger log; private Set<TransmissionConstraintChecker> pendingTcCheckers = new HashSet<>(); private final String instance,yprocName; ParameterValueList pvList = new ParameterValueList(); Processor yproc; int paramSubscriptionRequestId = -1; private final ScheduledThreadPoolExecutor timer; /** * Constructs a Command Queue Manager. * @param commandingManager * * @throws ConfigurationException in case there is an error in the configuration file. * Note: if the configuration file doesn't exist, this exception is not thrown. */ public CommandQueueManager(CommandingManager commandingManager) throws ConfigurationException { this.commandingManager = commandingManager; yproc=commandingManager.getChannel(); log = LoggingUtils.getLogger(this.getClass(), yproc); this.commandHistoryListener = yproc.getCommandHistoryPublisher(); this.commandReleaser = yproc.getCommandReleaser(); this.instance = yproc.getInstance(); this.yprocName = yproc.getName(); this.timer = yproc.getTimer(); CommandQueue cq=new CommandQueue(yproc, "default"); queues.put("default", cq); if (YConfiguration.isDefined("command-queue")) { YConfiguration config=YConfiguration.getConfiguration("command-queue"); List<String> queueList=config.getList("queueNames"); for(String qn:queueList) { if(!queues.containsKey(qn)) { queues.put(qn,new CommandQueue(yproc, qn)); } CommandQueue q=queues.get(qn); String state=config.getString(qn, "state"); q.state=CommandQueueManager.stringToQueueState(state); q.defaultState = q.state; if(config.containsKey(qn, "stateExpirationTimeS")) { q.stateExpirationTimeS = config.getInt(qn, "stateExpirationTimeS"); } q.roles=config.getList(qn, "roles"); if(config.containsKey(qn, "significances")) { q.significances = config.getList(qn, "significances"); } } } // schedule timer update to client timer.scheduleAtFixedRate(()->{ for(CommandQueue q : queues.values()) { if(q.state == q.defaultState && q.stateExpirationRemainingS > 0) { q.stateExpirationRemainingS = 0; notifyUpdateQueue(q); } else if(q.stateExpirationRemainingS >= 0) { log.trace("notifying update queue with new remaining seconds: {}", q.stateExpirationRemainingS); q.stateExpirationRemainingS--; notifyUpdateQueue(q); } } }, 1, 1, TimeUnit.SECONDS); } /** * called at processor startup, subscribe all parameters required for checking command constraints */ @Override public void doStart() { XtceDb xtcedb = yproc.getXtceDb(); Set<Parameter> paramsToSubscribe = new HashSet<>(); for(MetaCommand mc: xtcedb.getMetaCommands()) { if(mc.hasTransmissionConstraints()) { List<TransmissionConstraint> tcList = mc.getTransmissionConstraintList(); for(TransmissionConstraint tc: tcList) { paramsToSubscribe.addAll(tc.getMatchCriteria().getDependentParameters()); } } } if(!paramsToSubscribe.isEmpty()) { ParameterRequestManagerImpl prm = yproc.getParameterRequestManager(); try { paramSubscriptionRequestId = prm.addRequest(new ArrayList<>(paramsToSubscribe), this); } catch (InvalidIdentification e) { log.warn("Got Invalid identification when subscribing to parameters for command constraint check",e); notifyFailed(e); return; } } else { log.debug("No parameter required for post transmission contraint check"); } SystemParametersCollector sysParamCollector = SystemParametersCollector.getInstance(yproc.getInstance()); if(sysParamCollector!=null) { for(CommandQueue cq:queues.values()) { cq.setupSysParameters(); } sysParamCollector.registerProvider(this, null); } notifyStarted(); } @Override public void doStop() { if(paramSubscriptionRequestId!=-1) { ParameterRequestManagerImpl prm = yproc.getParameterRequestManager(); prm.removeRequest(paramSubscriptionRequestId); } notifyStopped(); } private static QueueState stringToQueueState(String state) throws ConfigurationException { if("enabled".equalsIgnoreCase(state)) { return QueueState.ENABLED; } if("disabled".equalsIgnoreCase(state)) { return QueueState.DISABLED; } if("blocked".equalsIgnoreCase(state)) { return QueueState.BLOCKED; } throw new ConfigurationException("'"+state+"' is not a valid queue state. Use one of enabled, disabled or blocked"); } public Collection<CommandQueue> getQueues() { return queues.values(); } public CommandQueue getQueue(String name) { return queues.get(name); } /** * Called from the CommandingImpl to add a command to the queue * First the command is added to the command history * Depending on the status of the queue, the command is rejected by setting the CommandFailed in the command history * added to the queue or directly sent using the uplinker * * @param authToken * @param pc * @return the queue the command was added to * @throws InvalidAuthenticationToken */ public synchronized CommandQueue addCommand(AuthenticationToken authToken, PreparedCommand pc) throws InvalidAuthenticationToken { commandHistoryListener.addCommand(pc); CommandQueue q = getQueue(authToken, pc); q.add(pc); notifyAdded(q, pc); if(q.state==QueueState.DISABLED) { q.remove(pc, false); failedCommand(q, pc, "Commanding Queue disabled", true); notifyUpdateQueue(q); } else if(q.state==QueueState.BLOCKED) { // notifyAdded(q, pc); } else if(q.state==QueueState.ENABLED) { if(pc.getMetaCommand().hasTransmissionConstraints()) { startTransmissionConstraintChecker(q, pc); } else { addToCommandHistory(pc.getCommandId(), CommandHistoryPublisher.TransmissionContraints_KEY, "NA"); q.remove(pc, true); releaseCommand(q, pc, true, false); } } return q; } private void startTransmissionConstraintChecker(CommandQueue q, PreparedCommand pc) { TransmissionConstraintChecker constraintChecker = new TransmissionConstraintChecker(q, pc); pendingTcCheckers.add(constraintChecker); scheduleImmediateCheck(constraintChecker); } private void onTransmissionContraintCheckPending(TransmissionConstraintChecker tcChecker) { addToCommandHistory(tcChecker.pc.getCommandId(), CommandHistoryPublisher.TransmissionContraints_KEY, "PENDING"); } private void onTransmissionContraintCheckFinished(TransmissionConstraintChecker tcChecker) { PreparedCommand pc = tcChecker.pc; CommandQueue q = tcChecker.queue; TCStatus status = tcChecker.aggregateStatus; log.info("transmission constraint finished for {} status: {}", pc.getCmdName(),status); pendingTcCheckers.remove(tcChecker); if(q.getState()==QueueState.BLOCKED) { log.debug("Command queue for command {} is blocked, leaving command in the queue", pc); return; } if(q.getState()==QueueState.DISABLED) { log.debug("Command queue for command {} is disabled, dropping command", pc); q.remove(pc, false); } if(!q.remove(pc, true)) { return; //command has been removed in the meanwhile } if(status==TCStatus.OK) { addToCommandHistory(pc.getCommandId(), CommandHistoryPublisher.TransmissionContraints_KEY, "OK"); releaseCommand(q, pc, true, false); } else if(status == TCStatus.TIMED_OUT) { addToCommandHistory(pc.getCommandId(), CommandHistoryPublisher.TransmissionContraints_KEY, "NOK"); failedCommand(q, pc, "Transmission constraints check failed", true); } } // Notify the monitoring clients private void notifyAdded(CommandQueue q, PreparedCommand pc) { for(CommandQueueListener m:monitoringClients) { try { m.commandAdded(q, pc); } catch (Exception e) { log.warn("got exception when notifying a monitor, removing it from the list", e); monitoringClients.remove(m); } } notifyUpdateQueue(q); } // Notify the monitoring clients private void notifySent(CommandQueue q, PreparedCommand pc) { for(CommandQueueListener m:monitoringClients) { try { m.commandSent(q, pc); } catch (Exception e) { log.warn("got exception when notifying a monitor, removing it from the list", e); monitoringClients.remove(m); } } notifyUpdateQueue(q); } private void notifyUpdateQueue(CommandQueue q) { for(CommandQueueListener m:monitoringClients) { try { m.updateQueue(q); } catch (Exception e) { log.warn("got exception when notifying a monitor, removing it from the list", e); monitoringClients.remove(m); } } } public void addToCommandHistory(CommandId commandId, String key, String value) { commandHistoryListener.updateStringKey(commandId, key, value); } /** * send a negative ack for a command. * @param pc the prepared command for which the negative ack is sent * @param notify notify or not the monitoring clients. */ private void failedCommand(CommandQueue cq, PreparedCommand pc, String reason, boolean notify) { addToCommandHistory(pc.getCommandId(), CommandHistoryPublisher.CommandFailed_KEY, reason); //Notify the monitoring clients if(notify) { for(CommandQueueListener m:monitoringClients) { try { m.commandRejected(cq, pc); } catch (Exception e) { log.warn("got exception when notifying a monitor, removing it from the list", e); monitoringClients.remove(m); } } } } private void releaseCommand(CommandQueue q, PreparedCommand pc, boolean notify, boolean rebuild) { //start the verifiers MetaCommand mc = pc.getMetaCommand(); if(mc.hasCommandVerifiers()) { log.debug("Starting command verification for {}", pc); CommandVerificationHandler cvh = new CommandVerificationHandler(yproc, pc); cvh.start(); } commandReleaser.releaseCommand(pc); //Notify the monitoring clients if(notify) { notifySent(q, pc); } } /** * @param authToken * @param pc * @return the queue where the command should be placed. * @throws InvalidAuthenticationToken */ public CommandQueue getQueue(AuthenticationToken authToken, PreparedCommand pc) throws InvalidAuthenticationToken { Privilege priv = Privilege.getInstance(); if(authToken == null || !priv.isEnabled()){ return queues.get("default"); } String[] roles = priv.getRoles(authToken); if(roles==null) { return queues.get("default"); } for(String role:roles) { for(CommandQueue cq:queues.values()) { if(cq.roles==null){ continue; } for(String r1:cq.roles) { if(role.equals(r1)){ if(cq.significances == null || (pc.getMetaCommand().getDefaultSignificance() == null && cq.significances.contains("none")) || (pc.getMetaCommand().getDefaultSignificance() != null && cq.significances.contains(pc.getMetaCommand().getDefaultSignificance().getConsequenceLevel().name()))) { // return first queue that matches the role of the user and significance of the command return cq; } } } } } return queues.get("default"); } /** * Called by external clients to remove a command from the queue * @param commandId * @param username - the username rejecting the command * @return the command removed from the queeu */ public synchronized PreparedCommand rejectCommand(CommandId commandId, String username) { log.info("called to remove command: {}", commandId); PreparedCommand pc=null; CommandQueue queue=null; for(CommandQueue q:queues.values()) { for(PreparedCommand c:q.getCommands()) { if(c.getCommandId().equals(commandId)) { pc = c; queue = q; break; } } } if(pc!=null) { queue.remove(pc, false); failedCommand(queue, pc, "Commmand rejected by "+username, true); notifyUpdateQueue(queue); } else { log.warn("command not found in any queue"); } return pc; } // Used by REST API as a simpler identifier public synchronized PreparedCommand rejectCommand(UUID uuid, String username) { for(CommandQueue q:queues.values()) { for(PreparedCommand pc:q.getCommands()) { if(pc.getUUID().equals(uuid)) { return rejectCommand(pc.getCommandId(), username); } } } log.warn("no prepared command found for uuid {}", uuid); return null; } /** * Called from external client to release a command from the queue * @param commandId * @param rebuild - if to rebuild the command binary from the source * @return the prepared command sent */ public synchronized PreparedCommand sendCommand(CommandId commandId, boolean rebuild) { PreparedCommand command=null; CommandQueue queue = null; for(CommandQueue q:queues.values()) { for(PreparedCommand pc:q.getCommands()) { if(pc.getCommandId().equals(commandId)) { command = pc; queue = q; break; } } } if(command!=null) { queue.remove(command, true); releaseCommand(queue, command, true, rebuild); } return command; } // Used by REST API as a simpler identifier public synchronized PreparedCommand sendCommand(UUID uuid, boolean rebuild) { for(CommandQueue q:queues.values()) { for(PreparedCommand pc:q.getCommands()) { if(pc.getUUID().equals(uuid)) { return sendCommand(pc.getCommandId(), rebuild); } } } log.warn("no prepared command found for uuid {}", uuid); return null; } /** * Called from external clients to change the state of the queue * @param queueName the queue whose state has to be set * @param newState the new state of the queue * @return the queue whose state has been changed or null if no queue by the name exists */ public synchronized CommandQueue setQueueState(String queueName, QueueState newState/*, boolean rebuild*/) { CommandQueue queue =null; for(CommandQueue q:queues.values()) { if(q.name.equals(queueName)) { queue=q; break; } } if(queue==null){ return null; } if(queue.state == newState) { if(queue.stateExpirationJob != null && newState != queue.defaultState) { log.debug("same state selected, resetting expiration time"); // reset state expiration date scheduleStateExpiration(queue); // Notify the monitoring clients notifyUpdateQueue(queue); } return queue; } queue.state = newState; if(queue.state==QueueState.ENABLED) { for(PreparedCommand pc:queue.getCommands()) { if(pc.getMetaCommand().hasTransmissionConstraints()) { startTransmissionConstraintChecker(queue, pc); } else { releaseCommand(queue, pc, true, false); } } queue.clear(true); } if(queue.state==QueueState.DISABLED) { for(PreparedCommand pc:queue.getCommands()) { failedCommand(queue, pc, "Commanding Queue disabled", true); } queue.clear(false); } if(queue.stateExpirationTimeS > 0 && newState != queue.defaultState) { log.info("scheduling expiration state for new state {} for queue {}", newState, queue.name); scheduleStateExpiration(queue); } // Notify the monitoring clients notifyUpdateQueue(queue); return queue; } private void scheduleStateExpiration(final CommandQueue queue) { if(queue.stateExpirationJob != null) { log.debug("expiration job existing, removing..."); queue.stateExpirationJob.cancel(false); queue.stateExpirationJob = null; } Runnable r = () -> { log.info("executing epiration state, reverting to {}", queue.defaultState); setQueueState(queue.name, queue.defaultState); queue.stateExpirationJob = null; }; log.info("sceduling expiration time in {}", queue.stateExpirationTimeS); queue.stateExpirationRemainingS = queue.stateExpirationTimeS; queue.stateExpirationJob = timer.schedule(r , queue.stateExpirationTimeS, TimeUnit.SECONDS); } /** * Called from a queue monitor to register itself in order to be notified when * new commands are added/removed from the queue. * @param cqm the callback which will be called with updates */ public void registerListener(CommandQueueListener cqm) { monitoringClients.add(cqm); } public boolean removeListener(CommandQueueListener cqm) { return monitoringClients.remove(cqm); } public String getInstance() { return instance; } public String getChannelName() { return yprocName; } private void doUpdateItems(final List<ParameterValue> items) { pvList.addAll(items); //remove old parameter values for(ParameterValue pv:items) { Parameter p = pv.getParameter(); int c = pvList.count(p); for(int i=0; i<c-1;i++) { pvList.removeFirst(p); } } for(TransmissionConstraintChecker tcc: pendingTcCheckers) { scheduleImmediateCheck(tcc); } } private void scheduleImmediateCheck(final TransmissionConstraintChecker tcc) { timer.execute(tcc::check); } private void scheduleCheck(final TransmissionConstraintChecker tcc, long millisec) { timer.schedule(tcc::check , millisec, TimeUnit.MILLISECONDS); } @Override public void updateItems(int subscriptionId, final List<ParameterValue> items) { timer.execute( () -> doUpdateItems(items)); } enum TCStatus {INIT, PENDING, OK, TIMED_OUT} class TransmissionConstraintChecker { List<TransmissionConstraintStatus> tcsList = new ArrayList<>(); final PreparedCommand pc; final CommandQueue queue; TCStatus aggregateStatus=TCStatus.INIT; public TransmissionConstraintChecker(CommandQueue queue, PreparedCommand pc) { this.pc = pc; this.queue = queue; List<TransmissionConstraint> constraints = pc.getMetaCommand().getTransmissionConstraintList(); for(TransmissionConstraint tc: constraints) { TransmissionConstraintStatus tcs = new TransmissionConstraintStatus(tc); tcsList.add(tcs); } } public void check() { long now = System.currentTimeMillis(); if(aggregateStatus==TCStatus.INIT) { //make sure that if timeout=0, the first check will not appear to be too late for(TransmissionConstraintStatus tcs: tcsList) { tcs.expirationTime = now + tcs.constraint.getTimeout(); } aggregateStatus = TCStatus.PENDING; } if(aggregateStatus!=TCStatus.PENDING) { return; } CriteriaEvaluator condEvaluator = new CriteriaEvaluatorImpl(pvList); aggregateStatus = TCStatus.OK; long scheduleNextCheck = Long.MAX_VALUE; for(TransmissionConstraintStatus tcs: tcsList) { if(tcs.status == TCStatus.OK) { continue; } if(tcs.status == TCStatus.PENDING) { long timeRemaining = tcs.expirationTime - now; if(timeRemaining < 0) { tcs.status = TCStatus.TIMED_OUT; aggregateStatus = TCStatus.TIMED_OUT; break; } else { MatchCriteria mc = tcs.constraint.getMatchCriteria(); try { if(!mc.isMet(condEvaluator)) { if(timeRemaining > 0) { aggregateStatus = TCStatus.PENDING; if(timeRemaining <scheduleNextCheck) { scheduleNextCheck = timeRemaining; } } else { aggregateStatus = TCStatus.TIMED_OUT; break; } } } catch (Exception e) { e.printStackTrace(); } } } } if(aggregateStatus == TCStatus.PENDING) { onTransmissionContraintCheckPending(this); scheduleCheck(this, scheduleNextCheck); } else { onTransmissionContraintCheckFinished(this); } } } static class TransmissionConstraintStatus { TransmissionConstraint constraint; TCStatus status; long expirationTime; public TransmissionConstraintStatus(TransmissionConstraint tc) { this.constraint = tc; status = TCStatus.PENDING; } } @Override public Collection<ParameterValue> getSystemParameters() { List<ParameterValue> pvlist = new ArrayList<>(); long time = yproc.getCurrentTime(); for(CommandQueue cq: queues.values()) { cq.fillInSystemParameters(pvlist, time); } return pvlist; } }