Skip to content

Executor

Ashwin Vangipuram edited this page Sep 24, 2020 · 16 revisions

Executor

This executor's jobs is to understand the student API and execute the student code using the corresponding Runtime functions. This process is probably the most delicate and had the most design changes as it has to accommodate any type of student code. To understand the executor properly, you need to first understand the Python C API and our Student API.

Flow Chart

Details

This process will read the current mode from the shm_wrapper_aux which will be supplied by the net_handler. Then whenever the mode changes to AUTO or TELEOP, the main function will create a new subprocess to run the new mode, using fork and run_mode. If mode changes to IDLE, it will kill the previously running process using kill_subprocess. To avoid any robot safety concerns, we must also reset the robot parameters on IDLE, which is done with reset_params. The main function handling the mode changes will loop forever and is only cancellable by sending a SIGINT signal.

To actually have the student code call the student API functions, we need to insert the API functions into the student module's namespace. This is done in executor_init where the Python interpreter is initialized. The insertion is done by setting the student code's attributes Robot and Gamepad to the corresponding attributes in the student API.

Before the mode actually begins, we need to first send device subscription requests to the dev_handler to ensure that SHM is being updated with device data. To do this, we must parse the student code to see which devices and parameters they are using, then send the corresponding requests. These functions are in code_parser.pyx which is included in the studentapi.pyx namespace using the Cython include statement. Then in executor_init, we call make_device_subs from code_parser.pyx.

run_mode will call the <mode>_setup and the <mode>_main Python functions using run_py_function. These functions will end up calling the Python C API located in "Python.h" to run the Python functions in the given student code, which is assumed to be in studentcode.py. More can be read about the Python C API at https://python.readthedocs.io/en/stable/c-api/index.html.

Challenges

The last mode is CHALLENGE which is used to run the student's coding challenges. The subprocess is started the same as the other modes but will call run_challenges instead of run_mode. This function will start reading from the challenge UNIX socket that is listening to the net_handler for the challenge inputs. It then runs the challenges with its inputs using run_py_function. Finally, it will return the outputs as a string to the net_handler by sending over the challenge UNIX socket.

The tricky thing for the CHALLENGE mode is that it isn't idempotent like the other 2 modes since it requires an input. As a result, we need to ensure that if the CHALLENGE mode finishes or gets cancelled, it will set the mode to IDLE on exit.

More details on how all these functions work are in the function documentation in executor.c.

Caveats

One thing to note which can cause unwanted interactions is that the executor process is now linked dynamically, and so it shares the symbols for the shared memory and logger with the studentapi.pyx file. This means that if you initialize it once on executor.c, you don't need to initialize it in studentapi.pyx.

Another important caveat to be aware of is Python's global interpreter lock (GIL). The Python interpreter can run in only 1 thread at a time and which thread is running is the one that acquires the GIL. As a result, whenever any Python C API functions need to be called, you must always be careful to acquire the GIL first and then release it when you are done. To make it simpler, we designed the architecture such that each mode runs in a separate process instead of a separate thread, which avoids any issue with the Python interpreter state.