Dispatcher
To process code analysis requests, a dispatcher instance connects to the RabbitMQ broker configured
through the EVA_DISPATCHER_RABBITMQ_HOST environment variable on startup. Then, it creates a queue
in the amq.topic exchange if it does not exist already. If the connection cannot be established or
is lost, the dispatcher automatically retries at regular intervals. During temporary connection
failures, the queue remains available for some time instead of being removed immediately, allowing
pending requests to be processed once the connection is restored. Requests published before a
dispatcher instance starts are discarded unless clients create the queue themselves using the same
name and TTL configuration.
Request
Clients must send requests for code analysis to the broker as an AMQP message with the routing key
eva.request. Every request must include the VERSION header in the message properties, where the
value must match the major component of the Eva version (4.0.2). This allows clients and
dispatchers to detect incompatible protocol changes early.
Before sending a request, the client should create an exclusive queue in which the dispatcher can
put the response. The reply_to header of the request must then be set accordingly, so that the
response later arrives at this queue and can be processed by clients.
The message body must contain a JSON object with the following structure:
EvalRequest: {
stack_ref: string,
solution_files: EvaFile[],
include_files: EvaFile[] = [],
test_files: EvaFile[] = [],
capability: RequestedCapability {
compile: bool = False,
test: bool = False,
style: bool = False
},
params: { [key: string]: str }
}
stack_ref: The selector for the evaluation stack that should be used for analysis.solution_files: Code files to analyze.test_files: Test files whose test cases are to be run. (only whentest = true)include_files: Additional files required for compiling or running the code.capability:compile: Whether the code should be compiled.style: Whether a style check should be performed.test: Whether the tests should be run.
params: Additional evaluator-specific parameters. Refer to the documentation of the evaluation stack that should be used.
EvaFile: {
path: string
content: string
}
path: File name or relative path. For Java, the path must match the fully qualified class name. For example,de/hsrm/subato/Date.javaif the class has the FQCNde.hsrm.subato.Date.java. If the class has no package, the file name is sufficient. For most of the other languages, specifying the file name is sufficient as well.content: File content.
Code Analysis
The following diagram describes the analysis process with the java evaluator and a java19
environment that is provided through a Docker image.
After an incoming request has been validated and the metadata of the evaluation stack has been
queried through the Registry, a new container is started through the Docker Engine API. The
files from the request are then copied into the container. Execution then takes place through the
evaluator, which runs bash scripts or commands in the containers through the Docker Engine API
and processes their output.
Handling Non-Termination
If the code under test exceeds the configured execution timeout, the evaluator aborts execution instead of waiting indefinitely. This minimizes the waste of compute time and reduces the risk of a failure due to resource exhaustion.
Timeouts are typically caused by infinite loops or deadlocks during dynamic analysis. Evaluators should terminate only the affected test case and continue with the remaining ones whenever possible. Even if test execution cannot be completed, evaluators should always return a test result. This ensures that results remain consistent and allows the test case to be identified that caused the timeout.
The maximum execution time for a script or command can be configured with the following environment variables:
EVA_DISPATCHER_EV_COMPILE_TIMEOUTEVA_DISPATCHER_EV_STYLE_TIMEOUTEVA_DISPATCHER_EV_TEST_TIMEOUTEVA_DISPATCHER_EV_CODECOV_TIMEOUT
Parallelization and Resource Limits
The dispatcher can run up to EVA_DISPATCHER_DOCKER_MAX_INSTANCES containers in parallel.
Additional requests remain queued until a running container finishes. A dispatcher instance can
therefore effectively process up to EVA_DISPATCHER_DOCKER_MAX_INSTANCES requests in parallel.
The CPU and memory of containers are limited so that infinite loops or endless memory consumption do not affect the compute time and memory of the other containers (and do not crash the host system). These resource limits also improve determinism. Since test execution relies on timeouts, heavy CPU contention could otherwise cause correct code to exceed their timeout simply because other containers consume a significant share of the available CPU time.
Resource limits are configured through the environment variables EVA_DISPATCHER_DOCKER_CPU_QUOTA
and EVA_DISPATCHER_DOCKER_MEM_LIMIT. The CPU quota indicates the maximum number of CPUs available
to a container and can be specified with both integer and fractional values. Memory limits are
specified in the format <limit>k|m|g, e.g. 512m.
Network Access
By default, containers are started with the network_mode=host option, allowing them to access the
network. When issuing many requests at once, the network_mode=none option should be used if
network access is not required for analysis.
Studies
have shown that disabling network access can significantly improve container performance. The
network mode can be configured through the EVA_DISPATCHER_DOCKER_NETWORK_MODE environment
variable.
Caching of Docker Images
As Docker images can be rebuild or updated in the Docker image registry, the dispatcher needs to
check regularly if there is a new version available. Yet, this may require HTTP requests to the
registry, which can slow down request processing if this is performed before every analysis request.
To reduce this overhead, the dispatcher caches image metadata. The cache lifetime can be configured
with EVA_DISPATCHER_DOCKER_IMG_CACHE_TTL, where the value refers to the number of seconds. A value
of -1 indicates an infinite TTL, and the cache can be disabled by setting the value to 0.
Output Limiting
The code to analyze may produce hundreds of megabytes of output during testing. To prevent excessive
memory usage, the output can be limited with EVA_DISPATCHER_EV_MAX_OUTPUT_SIZE. This limit is also
passed to the evaluator, which is responsible for enforcing it during execution.
Orphaned Containers
If a dispatcher instance terminates unexpectedly, running containers cannot be stopped immediately due to technical limitations. To prevent orphaned containers from running indefinitely, every container is configured to shut down automatically after 10 minutes.
Response
On successful analysis, the dispatcher sends a response in JSON format back to the RabbitMQ broker. The response is then delivered to the reply queue specified by the client. The response has the following structure:
EvalResponse: {
success: bool
message: string
result: EvalResult
version: string
sysconf: SysConfig {
rt: {
type: string, # e.g. docker
# if type = 'docker', then the following fields are included:
mem_limit: string,
cpu_quota: float,
auto_shutdown: int,
network_mode: string
}
evaluator_opts: EvaluatorOptions {
max_output_size: int
compile_timeout: int
style_timeout: int
test_timeout: int
codecov_timeout: int
}
}
}
success:trueif the analysis completed successfully,falseotherwise. This indicates whether the analysis completed successfully, not whether the code passed all tests.message: Error message, only whensuccess = false.result: Result of the analysis, only whensuccess = true.version: Eva version of the dispatcher instance that processed the request.sysconf: Runtime configuration of the dispatcher instance that processed the request.
The result field has the following structure:
EvalResult: {
compilation: CompilationResult
test: TestResult
style: StyleResult
}
Whether a field is null depends on the requested capabilities and their dependencies. For example:
- If only compilation is requested,
testandstylearenull. - If testing is requested but compilation fails,
testisnullbecause the tests cannot be executed
Compilation
CompilationResult: {
compiling: bool
output: string
}
compiling: Indicates whether the code compiled successfully.output: Combined stdout and stderr produced by the compiler.
Test
TestResult: {
testcases: int
failed: int
passed: int
stats: TestcaseStats {
failures: int
errors: int
pending: int
timeouts: int
}
correctness: float
leaks: int
testsuite: Testsuite
output: str
coverage: Coverage
}
testsuite: Test results in TREX format. Since test case names are not necessarily unique across test files, eachTestcaseincludes an additionalidfield. The ID is composed of thenameandclass_nameattributes in the format<name>#<class_name>. Ifclass_nameis not present (e.g. in programming languages without a class concept), the ID is identical toname.testcases: Number of test cases.passed: Number of passed test cases.failed: Number of failed test cases (testcases-passed)stats: Number of test cases for each outcomecorrectness: Value between 0 and 1, computed as(testcases - failed) / testcases. Iftestcases = 0, then the value is also 0.leaks: Difference between the number of memory allocations and deallocations.output: Output produced by the test process itself, such as messages from the test runner. Unlikestdoutandstderr, which contain output generated by the analyzed code, this field contains output from the testing infrastructure.coverage: Code coverage results in CEXF format.
Style
StyleResult: {
errors: int
report: StyleReport {
errors: StyleError[]
tool: string
version: string
}
}
errors: Number of reported style violations.report: Style analysis results in STEX format