Skip to main content

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ösungsdateien
  • test_files: Test-Dateien, dessen Testfälle ausgeführt werden sollen. (nur bei test = 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 soll
    • style: Ob eine Style-Prüfung durchgeführt werden soll
    • test: 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 FQCN de.hsrm.subato.Date.java hat. Wenn die Klasse kein Package hat, ist der Wert nur Date.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 wenn success = false
  • result: Ergebnis der Auswertung, nur wenn success = true
  • version: Verwendete Eva-Version der Dispatcher-Instanz, welche die Auswertung übernommen hat
  • sysconf: 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 kompiliert
  • output ist die Kombination von stdout und stderr 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 Attribut id, welches den Testfall eindeutig identifiziert. Die ID wird aus den Attributen name und class_name im Format <name>#<class_name> zusammengesetzt. Wenn class_name nicht vorhanden ist (z.B. in C), entspricht die ID dem Wert name.
  • testcases: Anzahl der Testfälle
  • passed: Anzahl der bestandenen Testfälle
  • failed: Ergibt sich aus der Differenz total - 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. Wenn total = 0, dann ist correctness auch 0.
  • leaks: Ergibt sich aus der Differenz allocations - frees
  • output: Im Gegensatz zu stdout bzw. stderr umfasst output die Ausgabe des Testprozesses selbst. Dazu gehören z.B. Ausgaben des Test-Runners. stdout und stderr 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 Stylefehler
  • report: JSON-Repräsentation der Ergebnisse im STEX-Format