Skip to content

I need another command for my application

Pierre Lasorak edited this page Oct 15, 2024 · 1 revision

Extra commands

This is going to require changes in the FSM configuration that you are using, and potentially in drunc's FSMaction.

There are several things to consider:

  1. Are you absolutely sure you can't fit the behaviour you need into the existing DAQ FSM?
  2. When should user be allowed to run the command?
  3. Do you need to pass parameter(s) to the application at run time?

Adding a command without run time parameter

What is the goal? I want drunc to execute the following code in my module:

void MyFancyModule::do_reconfigure(const data_t& /*payload*/) // Note that we do not use payload here
{
   TLOG() << "Reconfiguring";
   reconfigure(); // whatever that may do
   TLOG() << "DONE Reconfiguring";
}

The first thing to do is to register this command in the constructor of the module:

MyFancyModule::MyFancyModule(const std::string& name):
 dunedaq::appfwk::DAQModule(name)
{
  register_command("reconfigure", &MyFancyModule::do_reconfigure);
  // ...
}

If you are not interested in the payload from the run control, you just need to change the FSM configuration. An example of FSMconfiguration can be found around here.

To create a new command, you need to first decide when the user should be able to use it. We are going to create a command that takes us from one state and lands us in the same state (loopback transition).

You can see all the transitions listed in the FSMconfiguration object, for example:

<obj class="FSMconfiguration" id="fsmConf-test">
 <!-- ... -->
 <rel name="transitions">
  <ref class="FSMtransition" id="conf"/>
  <ref class="FSMtransition" id="start"/>
  <ref class="FSMtransition" id="enable_triggers"/>
  <ref class="FSMtransition" id="disable_triggers"/>
  <ref class="FSMtransition" id="drain_dataflow"/>
  <ref class="FSMtransition" id="stop_trigger_sources"/>
  <ref class="FSMtransition" id="stop"/>
  <ref class="FSMtransition" id="scrap"/>
 </rel>
</obj>

A bit further, you can see the definition of the transitions themselves, for example:

<obj class="FSMtransition" id="scrap">
 <attr name="source" type="string" val="configured"/>
 <attr name="dest" type="string" val="initial"/>
</obj>

This tells the run control what happens when you issue scrap. It moves you from source: configured to destination (dest): initial state.

Let's assume that we want our reconfigure command to runs from the configured state:

<obj class="FSMtransition" id="reconfigure">
 <attr name="source" type="string" val="configured"/>
 <attr name="dest" type="string" val="configured"/>
</obj>

You see here, the reconfigure command takes you from the configured state and lands you back in the configured state.

Of course, you will need to add it in the FSMconfiguration transitions relations:

<obj class="FSMconfiguration" id="fsmConf-test">
 ...
 <rel name="transitions">
  <ref class="FSMtransition" id="conf"/>
  <ref class="FSMtransition" id="reconfigure"/> <!-- New -->
  <!-- ... -->
 </rel>
</obj>

If you attach that FSMconfiguration to your controllers (and note that each controller could have a different FSM, so you will need to change it for each FSMconfiguration used), when you start drunc, you should have access to reconfigure after conf.

But my command needs to run from >1 states!

Well, source supports regex, so you can always define your transition as:

<obj class="FSMtransition" id="reconfigure">
 <attr name="source" type="string" val="configured|ready|running|dataflow_drained"/>
 <attr name="dest" type="string" val=""/>
</obj>

... and now you can execute reconfigure from the configured, ready, running, dataflow_drained.

Note that if you do that, you must add an empty value in dest.

Adding a command with run time parameters

What is the goal? I want drunc to execute the following code in my module:

void MyFancyModule::do_reconfigure(const data_t& payload) // Note that the payload is important
{
   TLOG() << "Reconfiguring";
   reconfigure(payload["new_oks_file"]); // whatever that may do
   TLOG() << "DONE Reconfiguring";
}

First, follow all the instructions above.

Then, you need to define an FSMaction that defines the parameters you want from the user in the run control shell to the parameter in the application. This FSMaction should define pre_transition methods than do the conversion from userland to applicationland.

Creating an FSMaction

Right now, all the FSMaction are in drunc (that may change in the future). They are defined here.

Let's continue with our reconfigure example. Suppose one of the parameter I want to pass down to the user is a choice of the filename that is used to configure the daq_application (I know, we'd never do that, but whatever). Suppose that these files are somehow "keyed" such that the user doesn't need to actually type the full path, but rather an identifier:

  • config1 maps to /some/oks/file/somewhere.data.xml
  • config2 maps to /another/oks/file/somewhere.data.xml

So now, I want the shifter to be able to type:

# ...
drunc-unified-shell > reconfigure --configuration-identifier config1
# ...
drunc-unified-shell > reconfigure --configuration-identifier config2

To do that, you can create a file called reconfigurer.py in drunc/src/fsm/actions with the following code:

from drunc.fsm.core import FSMAction
from drunc.exceptions import DruncException

def UnknownReconfigurationKey(DruncException):
    pass

class Reconfigurer(FSMAction):
    def __init__(self, configuration):
        super().__init__(
            name = "reconfigurer"
        )
        self.configuration_map = self.parse(configuration)

    def pre_reconfigure(self, _input_data, _context, configuration_identifier:str, **kwargs):
        file_name = None

        if configuration_identifier not in self.configuration_map:
            raise UnknownReconfigurationKey(configuration_identifier)

        _input_data['new_oks_file'] = self.configuration_map[configuration_identifier]
        return _input_data

    def parse(self, oks_conf):
        pass # AKA pain and tears

What's happening here?

  • We create an UnknownReconfigurationKey exception from DruncException (when you are throwing exception that you know about, please always inherit them from DruncException or its derivative).
  • We create an FSMaction called Reconfigurer:
    • The __init__ method names the FSMaction, and uses the FSMaction OKS configuration to configure itself. The parse method is left to the imagination of the reader. The configuration will be discussed a bit more later on.
    • The pre_reconfigure method gets executed right before we send reconfigure to the children (application or controller). Some important points:
      1. The name of the function must be fpre_{command_name}.
      2. The parameters of this function are very very (very) important:
        • _input_data is a dictionary that get shipped to the DAQ module as is, so this "new_oks_file" entry is what the C++ will need to use to access this parameter (see the C++ example a bit higher).
        • _context is the controller object (it's sometimes useful to know who sent the command etc. as seen in this example)
        • There should always be a **kwargs argument, that you are not allowed to use in this method (basically, they are the arguments of other actions).
        • The rest of the parameters are fed back to the user in the shell:
          1. They must have a type annotation (and they can only be bool, int, string, or float)
          2. They can have a default, in which case the argument in the shell will be non-mandatory. If they don't, they will be mandatory (like in this case, the user will not be able to issue reconfigure, they will always have to specify reconfigure --configuration-identifier).
          3. Bad things can happen if 2 FSMaction use the same parameter name for the same transition (I've never tested it, but I pretty much guarantee it)
      3. This function must always return a dictionary _input_data, even if it doesn't do anything to it.
      4. Anything in between that is more or less free (create a file, connect to ELisA, issue calls to a microservice, or more prosaically, ask the run number and run type to the user).

Finally, the action needs to be registered in drunc by adding it in the FSMActionFactory here.

OK, that concludes the changes to drunc.

We are not done though. If we want our Reconfigurer action to be used, we have to go and modify OKS once more.

Linking an FSMaction to a FSMConfiguration

Note: I know this isn't nice, we are working on getting this better and redesigning the FSM schema.

First thing is the configuration of the FSMAction itself, for this you need to add:

<obj class="FSMaction" id="reconfigurer">
 <attr name="name" type="string" val="reconfigurer"/> <!-- Isn't this the same as above? Yes it is -->
 <rel name="parameters">
  <ref class="Variable" id="config1-key"/>
  <ref class="Variable" id="config2-key"/>
 </rel>
</obj>

<obj class="Variable" id="config1-key">
 <attr name="name" type="string" val="config1-key"/>
 <attr name="value" type="string" val="/some/oks/file/somewhere.data.xml"/>
</obj>

<!-- ... -->

As you see, the FSMaction can only have key-value configuration, which are all in the parameters relationship. Yes, this isn't great, but believe me, you do not want to go and define a schema for every FSMaction (at least, not right now).

Now, let's hook our action into the FSMconfiguration. First, we need to tell drunc we want to use this action, so let's add it in the actions relationships:

<obj class="FSMconfiguration" id="fsmConf-test">
 <!-- ... -->
 <rel name="actions">
  <!-- ... -->
  <ref class="FSMaction" id="reconfigurer"/>
 </rel>
</obj>

Then, we need to say when and how the pre_reconfigure action gets executed. In this case, this is a bit useless, however, there are cases where the execution order of these actions matter. Similarly, if an exception get thrown by the action, whether you want to abort the execution of the transition needs to be configured. All of the above information is encoded in the FSMxTransition configuration object (where x stands either for pre or post).

Let's have a look at an example for a pre start transition:

<obj class="FSMxTransition" id="pre_start_test">
 <attr name="transition" type="string" val="start"/>
 <attr name="order" type="string">
  <data val="user-provided-run-number"/>
  <data val="file-run-registry"/>
 </attr>
 <attr name="mandatory" type="string">
  <data val="user-provided-run-number"/>
  <data val="file-run-registry"/>
 </attr>
</obj>

You can see that the order at which the actions are executed is in the order attribute. Similarly, if an exception get thrown in any of the mandatory actions, the transition is interrupted (in this case, if the user didn't provide a viable run type, run number or trigger rate, and if the configuration cannot be saved in PWD).

So, our reconfigure pre_transition could look like:

<obj class="FSMxTransition" id="pre_reconfigure">
 <attr name="transition" type="string" val="reconfigure"/>
 <attr name="order" type="string">
  <data val="reconfigurer"/>
 </attr>
 <attr name="mandatory" type="string">
  <data val="reconfigurer"/>
 </attr>
</obj>

We can then add our pre_reconfigure in our FSMConfiguration:

<obj class="FSMconfiguration" id="fsmConf-test">
 <!-- ... -->
 <rel name="pre_transitions">
  <ref class="FSMxTransition" id="pre_reconfigure"/>
 </rel>
 <!-- ... -->
</obj>

And that's it.

Couple of points:

  • You generally only want to execute FSMAction once, this means that the root-controller is likely to have all the pre_transitions/post_transitions/actions and none of the other controllers' FSM configuration. Remember though, you still need to add the reconfigure transition in all the FSMConfiguration of all the controllers.
  • pre_transitions run before the transition post_transitions run after the transition. You cannot inject data in post_transition to get to the application, because the transition has already happen, however it is practical to use post transitions for other things (such as thread pinning).
Clone this wiki locally