Dispatcher
Soll eine Instanz für die Auswertung eingesetzt werden, muss diese eine Verbindung zum RabbitMQ
Broker aufbauen können. Eine Dispatcher-Instanz verbindet sich beim Start zum RabbitMQ Broker
(Umgebungsvariable EVA_DISPATCHER_RABBITMQ_HOST
) und legt eine Queue in der Exchange amq.topic
an, wenn diese noch nicht existiert. Bricht die Verbindung zum Broker ab oder ist bei Start des
Dispatchers nicht möglich, wird regelmäßig ein erneuter Verbindungsaufbau unternommen. Die Queue
bleibt für den Fall von temporären Verbindungsabbrüchen noch eine Weile bestehen und wird nicht
sofort gelöscht um ausstehende Anfragen bei Wiederaufbau der Verbindung noch bearbeiten zu können.
Anfragen, die vor dem Start der ersten Dispatcher-Instanz an den Broker gestellt werden, werden
verworfen. Außer, Clients erstellen beim Start eine Queue mit dem selben Namen und der selben
TTL-Konfiguration.
Anfrage
Clients müssen Anfragen zur Lösungsauswertung im JSON-Format mit dem Routing Key
eva.v<API_VERSION>.request
an den Broker senden. API_VERSION
ist mit der aktuellen API-Version
(nicht Eva Version) zu ersetzen und stellt sicher, dass inkompatible Änderungen an der
Schnittstelle frühzeitig erkannt werden. Die aktuelle API Version wird in den Client-Bibliotheken
(z.B. eva.com
oder eva-java
) über Konstanten definiert.
Vor dem Senden der Anfrage sollte sich der Client eine exklusive Queue anlegen, in der die
Dispatchers die Antworten ablegen. Der reply_to
Header ist dann entsprechend zu setzen, damit die
Antwort später an dieser Queue ankommt und von Clients verarbeitet werden kann. Wenn keine Antwort
kommt, ist zu prüfen, ob es ein aktives Dispatcher mit der verwendeten API_VERSION
gibt.
Eine Anfrage sieht so aus:
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
: Der Selektor (siehe Registry) für den konkreten Evaluation Stack, der die Anfrage bearbeiten soll.solution_files
: Lösungsdateientest_files
: Test-Dateien, dessen Testfälle ausgeführt werden sollen. (nur beitest = true
)include_files
: Dateien, die für die Kompilierung oder die Ausführung der Lösung notwendig sind.capability
:compile
: Ob die Lösung kompiliert werden sollstyle
: Ob eine Style-Prüfung durchgeführt werden solltest
: Ob die Lösung getestet werden soll
params
: Zusätzliche Parameter, je nach Evaluation Stack (siehe Doku des jeweiligen Stacks).
EvaFile: {
path: string
content: string
}
path
: Dateiname bzw. relativer Dateipfad für die Kodierung des Fully-Qualified Class Name. Am Beispiel von Java:de/hsrm/subato/Date.java
wenn die Klasse den FQCNde.hsrm.subato.Date.java
hat. Wenn die Klasse kein Package hat, ist der Wert nurDate.java
. Für andere Sprachen genügt die Angabe des Dateinamens.content
: Inhalt der Datei
Lösungsauswertung
Der folgende Abschnitt beschreibt die Auswertung mit Docker als Laufzeitumgebung. Bisher ist nur eine Auswertung über Docker möglich, die Laufzeitumgebung kann aber flexibel ausgetauscht werden.
Nachdem eine eingehende Anfrage geprüft wurde und die Metadaten des Evaluation Stacks über die
Registry
abgefragt wurden, wird ein neuer Container über die Docker Engine API gestartet. Dabei
werden die Dateien aus der Anfrage in den Container kopiert. Daraufhin erfolgt die Ausführung über
den Evaluator, der Bash Skripte oder Befehle in den Containern über die Docker Engine API ausführt
und die Ergebnisse über die Standardausgabe verarbeitet.
Umgang mit Nicht-Terminierung
Falls die Auswertung nicht innerhalb einer konfigurierten Zeit terminiert, wird nicht weiter auf eine Terminierung gewartet. Damit wird die Verschwendung von Rechenzeit minimiert und das Risiko für einen Ausfall durch Überlastung reduziert.
Zu dieser Situation kommt es vor allem dann, wenn beim Testen von Lösungen Endlosschleifen auftreten oder es zu einem Dead-Lock kommt. Damit es nicht zu einer erzwungenen Terminierung kommt, hat jeder Evaluator dafür Sorge zu tragen, mit diesem Problem umzugehen. Ein Testfall sollte in diesem Fall vorzeitig abgebrochen und mit dem nächsten fortgefahren werden. Grundsätzlich muss ein Evaluator immer ein Testergebnis zurückgegeben, auch wenn die Tests nicht fortgesetzt werden. So können Testergebnisse konsistent gespeichert werden und es ist ebenfalls ersichtlich, welcher Testfall das Problem verursacht hat.
Die maximale Ausführungszeit für ein Skript oder einen Befehl kann mit den Umgebungsvariablen
EVA_DISPATCHER_EV_COMPILE_TIMEOUT
, EVA_DISPATCHER_EV_STYLE_TIMEOUT
,
EVA_DISPATCHER_EV_TEST_TIMEOUT
und EVA_DISPATCHER_EV_CODECOV_TIMEOUT
konfiguriert werden.
Parallelisierung und CPU/RAM Begrenzungen
Das Dispatcher erlaubt EVA_DISPATCHER_DOCKER_MAX_INSTANCES
parallel laufende Container. Weitere
Container werden erst dann gestartet, wenn ein bestehender Container terminiert. Eine
Dispatcher-Instanz kann effektiv also maximal EVA_DISPATCHER_DOCKER_MAX_INSTANCES
Lösungen
parallel auswerten.
Die CPU und der Arbeitsspeicher von Containern werden begrenzt, damit Lösungen mit Endlosschleifen
oder endlosem Speicherverbrauch die Rechenzeit und den Speicher der anderen Container nicht
beeinflussen (und das Host-System nicht zum Absturz bringen). Das ist auch wichtig um das Risiko für
nichtdeterministische Ergebnisse zu verringern. Das Risiko entsteht dadurch, dass eine maximale
Ausführungszeit für die Tests verwendet wird. Ohne Begrenzung von CPU und Arbeitsspeicher kann es
bei der parallen Lösungsauswertung dazu kommen, dass terminierende Lösungen fälschlicherweise als
nicht-terminierend betrachtet werden, weil parallel eine andere Lösung einen Großteil der CPU-Zeit
der Maschine in Anspruch nimmt. Die Grenzen können über die Umgebungsvariablen
EVA_DISPATCHER_DOCKER_CPU_QUOTA
und EVA_DISPATCHER_DOCKER_MEM_LIMIT
konfiguriert werden. Die CPU
Quota ist die maximal Anzahl der CPUs, die pro Container verwendet werden dürfen. Die CPU Quota kann
entweder mit ganzen Zahlen oder Bruchteilen (z.B. 1.5 CPUs) angegeben werden. Das Speicherlimit wird
im Format <limit>k|m|g
angegeben, z.B. 512m
.
Netzwerkzugriff
Container werden standardmäßig mit der network_mode=host
Option gestartet und haben damit Zugriff
auf das Netzwerk. Für die Massenauswertung von Lösungen sollte unbedingt network_mode=none
verwendet werden wenn für die Auswertung kein Zugriff auf das Netzwerk notwendig ist.
Studien
haben gezeigt, dass die network_mode
Option erhebliche Auswirkungen auf die Leistung haben kann.
Die Konfiguration kann mit der Umgebungsvariable EVA_DISPATCHER_DOCKER_NETWORK_MODE
erfolgen.
Caching von Docker Images
Docker Images der Umgebungen können sich in der Container Registry ändern. Um zu überprüfen, ob das
neueste Image lokal vorhanden ist, sind HTTP Anfragen an die Container Registry notwendig. Das
sollte nicht bei jeder Anfrage erfolgen, da dies die Lösungsauswertung unnötig verlangsamt. Daher
wird ein Cache verwendet, dessen TTL mit EVA_DISPATCHER_DOCKER_IMG_CACHE_TTL
konfiguriert werden
kann. Die Angabe erfolgt über die Anzahl der Sekunden. Ein Wert von -1 bedeutet, dass die TTL
unendlich ist. Deaktiviert werden kann der Cache mit EVA_DISPATCHER_DOCKER_IMG_CACHE_TTL=0
Begrenzung von Ausgaben
Es kann vorkommen, dass Lösungen bei der Testausführung viele Ausgaben erzeugen, die zu großen
Datenmengen (mehrere Hundert MB) führen. Um die Ausgaben zu begrenzen, sollte die Anzahl der Zeichen
mit EVA_DISPATCHER_EV_MAX_OUTPUT_SIZE
begrenzt werden. Dieser Wert wird auch an die Evaluators
weitergegeben und muss individuell berücksichtigt werden, sonst kommt es ggf. zu Fehlern bei der
Ausführung.
Verwaiste Container
Weiterhin ist zu beachten, dass bei einer abrupten Terminierung einer Dispatcher-Instanz die gestarteten Container aufgrund von technischen Limitierungen nicht direkt gestoppt werden. Es wäre ein Neustart des Docker Daemons notwendig, um diese Container zu stoppen. Um zu vermeiden, dass solche Container nicht ewig im System aktiv bleiben, wird ein Auto-Shutdown bei Start der Container konfiguriert. Verwaiste Container terminieren daher nach 10 Minuten automatisch.
Antwort
Bei erfolgreicher Ausführung sendet der Dispatcher eine Antwort im JSON-Format an den RabbitMQ Broker zurück, die an den Client ausgeliefert wird. Die Antwort sieht so aus:
EvalResponse: {
success: bool
message: string
result: EvalResult
version: string
sysconf: SysConfig {
rt: {
type: string, # z.B. docker
# bei docker z.B.:
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
:true
wenn die Auswertung erfolgreich war,false
sonst. Damit ist nicht gemeint, ob die Lösung korrekt war sondern ob der Auswertungsprozess selbst erfolgreich durchgelaufen ist.message
: Fehlermeldung, nur wennsuccess = false
result
: Ergebnis der Auswertung, nur wennsuccess = true
version
: Verwendete Eva-Version der Dispatcher-Instanz, welche die Auswertung übernommen hatsysconf
: Konfiguration der Dispatcher-Instanz, welche die Anfrage bearbeitet hat
Das Ergebnis hat die folgende Struktur:
EvalResult: {
compilation: CompilationResult
test: TestResult
style: StyleResult
}
Ob die einzelnen Bestandteile != null
sind, hängt davon ab, welche Capabilities angefragt wurden
und ob es Abhängigkeiten dazwischen gibt. Beispiel: Wenn nur die Kompilierung angefragt wird, sind
test
und style
jeweils null
. Genauso kann test
gleich null
sein, obwohl es angefragt
wurde. Dies ist dann der Fall, wenn die Kompilierung bereits fehlgeschlagen ist.
Kompilierung
CompilationResult: {
compiling: bool
output: string
}
compiling
gibt an, ob die Lösung kompiliertoutput
ist die Kombination vonstdout
undstderr
des Compilers
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
: JSON-Repräsentation der Ergebnisse im TREX-Format. Namen von Testfällen sind nicht eindeutig und können in mehreren Testdateien auftreten.Testcase
hat in der JSON-Repräsentation daher ein weiteres Attributid
, welches den Testfall eindeutig identifiziert. Die ID wird aus den Attributenname
undclass_name
im Format<name>#<class_name>
zusammengesetzt. Wennclass_name
nicht vorhanden ist (z.B. in C), entspricht die ID dem Wertname
.testcases
: Anzahl der Testfällepassed
: Anzahl der bestandenen Testfällefailed
: Ergibt sich aus der Differenztotal
-passed
stats
: Besteht aus den Anzahlen der Testfälle, die das jeweilige Ergebnis (z.B.timeout
aufweisen)correctness
: Zwischen 0 und 1, ergibt sich aus(total - failed) / total
. Wenntotal = 0
, dann istcorrectness
auch 0.leaks
: Ergibt sich aus der Differenzallocations - frees
output
: Im Gegensatz zustdout
bzw.stderr
umfasstoutput
die Ausgabe des Testprozesses selbst. Dazu gehören z.B. Ausgaben des Test-Runners.stdout
undstderr
begrenzen sich (zumindest sollten sie das) auf die Ausgaben, die die von der Lösung selbst gemacht werden.coverage
: JSON-Repräsentation der Ergebnisse im CEXF-Format
Style
StyleResult: {
errors: int
report: StyleReport {
errors: StyleError[]
tool: string
version: string
}
}
errors
: Anzahl der Stylefehlerreport
: JSON-Repräsentation der Ergebnisse im STEX-Format