diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml
index 5f182801..688fc93f 100644
--- a/.github/workflows/workflow.yml
+++ b/.github/workflows/workflow.yml
@@ -13,7 +13,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- java_version: [ 17, 21 ]
+ java_version: [ 21 ]
steps:
- name: Checkout
uses: actions/checkout@v3
@@ -32,10 +32,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v3
- - name: Set up JDK 17
+ - name: Set up JDK 21
uses: actions/setup-java@v3
with:
- java-version: 17
+ java-version: 21
distribution: 'temurin'
- name: Set outputs
id: vars
diff --git a/Dockerfile b/Dockerfile
index 280e032c..e9be27cb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,18 +1,22 @@
-FROM maven:3-openjdk-17-slim AS build
-WORKDIR /build
-COPY pom.xml pom.xml
-RUN mvn dependency:go-offline --no-transfer-progress -Dmaven.repo.local=/mvn/.m2nrepo/repository
-COPY src/ src/
-RUN mvn package --no-transfer-progress -DskipTests -Dmaven.repo.local=/mvn/.m2nrepo/repository
-
-#
-# Package stage
-#
-FROM openjdk:17-alpine
+FROM eclipse-temurin:21-jdk-jammy AS builder
WORKDIR /app
-RUN addgroup -S javagroup && adduser -S javauser -G javagroup && mkdir data
-COPY --from=build /build/target/cws-k8s-scheduler*.jar cws-k8s-scheduler.jar
-RUN chown -R javauser:javagroup /app
-USER javauser
-EXPOSE 8080
-ENTRYPOINT ["java","-jar","/app/cws-k8s-scheduler.jar"]
+
+RUN apt-get update && apt-get install -y maven && rm -rf /var/lib/apt/lists/*
+
+COPY pom.xml .
+RUN mvn dependency:go-offline --no-transfer-progress
+
+COPY src/ ./src/
+RUN mvn package --no-transfer-progress -DskipTests
+
+FROM eclipse-temurin:21-jre-jammy
+
+WORKDIR /app
+
+RUN apt-get update && apt-get install -y \
+ libglib2.0-0 \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY --from=builder /app/target/cws-k8s-scheduler-*-SNAPSHOT.jar app.jar
+
+CMD ["java", "-jar", "app.jar"]
diff --git a/Dockerfile-development b/Dockerfile-development
index 445f1b5a..ecf73f63 100644
--- a/Dockerfile-development
+++ b/Dockerfile-development
@@ -1,4 +1,4 @@
-FROM maven:3-openjdk-17-slim AS build
+FROM maven:3-openjdk-18-slim AS build
WORKDIR /build
COPY pom.xml pom.xml
RUN mkdir data/ && mvn dependency:go-offline -B -Dmaven.repo.local=/mvn/.m2nrepo/repository
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 00000000..2b77db2c
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,136 @@
+pipeline {
+ agent {
+ kubernetes {
+ yamlFile 'jenkins-pod.yaml'
+ }
+ }
+ environment {
+ // creates DOCKERHUB_USR and DOCKERHUB_PSW env variables
+ DOCKERHUB = credentials('fondahub-dockerhub')
+ }
+
+ stages {
+ stage('Build') {
+ steps {
+ container('maven') {
+ // run a clean build without tests to see if the project compiles
+ sh 'mvn clean test-compile -DskipTests=true -Dmaven.javadoc.skip=true -B -V'
+ }
+ }
+ }
+
+ stage('Test') {
+ steps {
+ container('maven') {
+ // run JUnit tests
+ sh 'mvn test -B -V'
+ }
+ }
+ post {
+ // collect test results
+ always {
+ junit 'target/surefire-reports/TEST-*.xml'
+ jacoco classPattern: 'target/classes,target/test-classes', execPattern: 'target/coverage-reports/*.exec', inclusionPattern: '**/*.class', sourcePattern: 'src/main/java,src/test/java'
+ archiveArtifacts 'target/surefire-reports/TEST-*.xml'
+ archiveArtifacts 'target/*.exec'
+ }
+ }
+ }
+
+ stage('Package') {
+ steps {
+ container('maven') {
+ sh 'mvn package -DskipTests=true -Dmaven.javadoc.skip=true -B -V'
+ }
+ }
+ post {
+ success {
+ archiveArtifacts 'target/*.jar'
+ }
+ }
+ }
+
+ stage('Static Code Analysis') {
+ steps {
+ container('maven') {
+ withSonarQubeEnv('fonda-sonarqube') {
+ sh '''
+ mvn sonar:sonar -B -V -Dsonar.projectKey=workflow_k8s_scheduler \
+ -Dsonar.branch.name=$BRANCH_NAME -Dsonar.sources=src/main/java -Dsonar.tests=src/test/java \
+ -Dsonar.inclusions="**/*.java" -Dsonar.test.inclusions="src/test/java/**/*.java" \
+ -Dsonar.junit.reportPaths=target/surefire-reports
+ '''
+ }
+ }
+ }
+ }
+
+ stage('Build and push Docker') {
+ // only push images from the master branch
+ when {
+ branch "master"
+ }
+ // agents are specified per stage to enable real parallel execution
+ parallel {
+ stage('workflow-k8s-scheduler') {
+ agent {
+ kubernetes {
+ yamlFile 'jenkins-pod.yaml'
+ }
+ }
+ steps {
+ container('hadolint') {
+ sh "hadolint --format json Dockerfile | tee -a hadolint_scheduler.json"
+ }
+ // build and push image to fondahub/workflow-k8s-scheduler
+ container('docker') {
+ sh "echo $DOCKERHUB_PSW | docker login -u $DOCKERHUB_USR --password-stdin"
+ sh "docker build . -t fondahub/workflow-k8s-scheduler:${GIT_COMMIT[0..7]}"
+ sh "docker tag fondahub/workflow-k8s-scheduler:${GIT_COMMIT[0..7]} fondahub/workflow-k8s-scheduler:latest"
+ sh "docker push fondahub/workflow-k8s-scheduler:${GIT_COMMIT[0..7]}"
+ sh "docker push fondahub/workflow-k8s-scheduler:latest"
+ }
+ }
+ post {
+ always {
+ archiveArtifacts "hadolint_scheduler.json"
+ recordIssues(
+ aggregatingResults: true,
+ tools: [hadoLint(pattern: "hadolint_scheduler.json", id: "scheduler")]
+ )
+ }
+ }
+ }
+ stage('vsftpd') {
+ agent {
+ kubernetes {
+ yamlFile 'jenkins-pod.yaml'
+ }
+ }
+ steps {
+ container('hadolint') {
+ sh "hadolint --format json daemons/ftp/Dockerfile | tee -a hadolint_vsftpd.json"
+ }
+ // build and push image to fondahub/vsftpd
+ container('docker') {
+ sh "echo $DOCKERHUB_PSW | docker login -u $DOCKERHUB_USR --password-stdin"
+ sh "docker build daemons/ftp/ -t fondahub/vsftpd:${GIT_COMMIT[0..7]}"
+ sh "docker tag fondahub/vsftpd:${GIT_COMMIT[0..7]} fondahub/vsftpd:latest"
+ sh "docker push fondahub/vsftpd:${GIT_COMMIT[0..7]}"
+ sh "docker push fondahub/vsftpd:latest"
+ }
+ }
+ post {
+ always {
+ archiveArtifacts "hadolint_vsftpd.json"
+ recordIssues(
+ aggregatingResults: true,
+ tools: [hadoLint(pattern: "hadolint_vsftpd.json", id: "vsfptd")]
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 14740999..b11996dd 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,8 @@
-# Common Workflow Scheduler for Kubernetes
-[](https://zenodo.org/badge/latestdoi/596122315)
+# Kubernetes Workflow Scheduler
-In this repository, you will find the Common Workflow Scheduler for Kubernetes proposed in the paper "**How Workflow Engines Should Talk to Resource Managers: A Proposal for a Common Workflow Scheduling Interface**."
+SWAGGER: http://localhost:8080/swagger-ui.html
+
+API-DOCS: http://localhost:8080/v3/api-docs/
---
#### Build
@@ -188,6 +189,7 @@ The following strategies are available:
| random | Randomly prioritize tasks. |
| max | Prioritize tasks with larger input size. |
| min | Prioritize tasks with smaller input size. |
+| wow | WOW scheduler for data location awareness. This is scheduling + node assignment. Details are provided in our paper [tbd](tbd). |
| Node Assignment Strategy | Behaviour |
|--------------------------|-----------------------------------------------------------------------------------------|
@@ -215,4 +217,4 @@ Lehmann Fabian, Jonathan Bader, Friedrich Tschirpke, Lauritz Thamsen, and Ulf Le
```
---
#### Acknowledgement:
-This work was funded by the German Research Foundation (DFG), CRC 1404: "FONDA: Foundations of Workflows for Large-Scale Scientific Data Analysis."
\ No newline at end of file
+This work was funded by the German Research Foundation (DFG), CRC 1404: "FONDA: Foundations of Workflows for Large-Scale Scientific Data Analysis."
diff --git a/daemons/ftp/Dockerfile b/daemons/ftp/Dockerfile
new file mode 100644
index 00000000..57099808
--- /dev/null
+++ b/daemons/ftp/Dockerfile
@@ -0,0 +1,17 @@
+FROM python:slim
+RUN apt update && apt install -y \
+ vsftpd \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN mkdir -p /var/run/vsftpd/empty
+
+COPY vsftpd.conf /etc/vsftpd.conf
+
+USER root
+RUN echo 'root:password' | chpasswd
+
+COPY ftp.py /code/ftp.py
+
+WORKDIR /code
+
+ENTRYPOINT ["sh","-c","/usr/sbin/vsftpd /etc/vsftpd.conf"]
\ No newline at end of file
diff --git a/daemons/ftp/ftp.py b/daemons/ftp/ftp.py
new file mode 100644
index 00000000..4a779dde
--- /dev/null
+++ b/daemons/ftp/ftp.py
@@ -0,0 +1,362 @@
+#!/usr/bin/env python3
+import ftplib
+import json
+import logging as log
+import os
+import shutil
+import signal
+import sys
+import time
+import urllib.request
+import urllib.parse
+from concurrent.futures import ThreadPoolExecutor
+from pathlib import Path
+from time import sleep
+
+########################################################################################
+# Call this class with three arguments: trace enabled, name to store logs, config json #
+########################################################################################
+
+exitIfFileWasNotFound = True
+CLOSE = False
+UNEXPECTED_ERROR = "Unexpected error"
+EXIT = 0
+log.basicConfig(
+ format='%(levelname)s: %(message)s',
+ level=log.DEBUG,
+ handlers=[
+ log.FileHandler(".command.init." + sys.argv[2] + ".log"),
+ log.StreamHandler()
+ ]
+)
+trace = {}
+traceFilePath = ".command.scheduler.trace"
+errors = 0
+
+
+def myExit(code):
+ global EXIT
+ EXIT = code
+ global CLOSE
+ CLOSE = True
+ writeTrace(trace)
+ exit(EXIT)
+
+
+def close(signalnum, syncFile):
+ log.info("Killed: %s", str(signalnum))
+ closeWithWarning(50, syncFile)
+
+
+def closeWithWarning(errorCode, syncFile):
+ syncFile.write('##FAILURE##\n')
+ syncFile.flush()
+ syncFile.close()
+ myExit(errorCode)
+
+
+def getIP(node, dns, execution):
+ ip = urllib.request.urlopen(dns + "daemon/" + execution + "/" + node).read()
+ return str(ip.decode("utf-8"))
+
+
+# True if the file was deleted or did not exist
+def clearLocation(path, dst=None):
+ if os.path.exists(path):
+ log.debug("Delete %s", path)
+ if os.path.islink(path):
+ if dst is not None and os.readlink(path) == dst:
+ return False
+ else:
+ os.unlink(path)
+ elif os.path.isdir(path):
+ shutil.rmtree(path)
+ else:
+ os.remove(path)
+ return True
+
+
+def getFTP(node, currentIP, dns, execution, syncFile):
+ global errors
+ connectionProblem = 0
+ while connectionProblem < 8:
+ try:
+ if currentIP is None:
+ log.info("Request ip for node: %s", node)
+ ip = getIP(node, dns, execution)
+ else:
+ ip = currentIP
+ log.info("Try to connect to %s", ip)
+ ftp = ftplib.FTP(ip, timeout=10)
+ ftp.login("root", "password")
+ ftp.set_pasv(True)
+ ftp.encoding = 'utf-8'
+ log.info("Connection established")
+ return ftp
+ except ConnectionRefusedError:
+ errors += 1
+ log.warning("Connection refused! Try again...")
+ except BaseException:
+ errors += 1
+ log.exception(UNEXPECTED_ERROR)
+ connectionProblem += 1
+ time.sleep(2 ** connectionProblem)
+ closeWithWarning(8, syncFile)
+
+
+def closeFTP(ftp):
+ global errors
+ if ftp is None:
+ return
+ try:
+ ftp.quit()
+ ftp.close()
+ except BaseException:
+ errors += 1
+ log.exception(UNEXPECTED_ERROR)
+
+
+def downloadFile(ftp, filename, size, index, node, syncFile, speed):
+ global errors
+ log.info("Download %s [%s/%s] - %s", node, str(index).rjust(len(str(size))), str(size), filename)
+ try:
+ syncFile.write("S-" + filename + '\n')
+ clearLocation(filename)
+ Path(filename[:filename.rindex("/")]).mkdir(parents=True, exist_ok=True)
+ start = time.time()
+ with open(filename, 'wb') as file:
+ if speed == 100:
+ ftp.retrbinary('RETR %s' % filename, file.write, 102400)
+ else:
+ timer = {"t": time.time_ns()}
+
+ def callback(data):
+ now = time.time_ns()
+ diff = now - timer["t"]
+ file.write(data)
+ timeToSleep = (diff * (100 / speed) - diff) / 1_000_000_000
+ # sleep at least 10ms
+ if timeToSleep > 0.01:
+ time.sleep(timeToSleep)
+ timer["t"] = time.time_ns()
+
+ ftp.retrbinary('RETR %s' % filename, callback, 102400)
+ end = time.time()
+ sizeInMB = os.path.getsize(filename) / 1048576
+ delta = (end - start)
+ log.info("Speed: %.3f Mb/s", sizeInMB / delta)
+ return sizeInMB, delta
+ except ftplib.error_perm as err:
+ errors += 1
+ if str(err) == "550 Failed to open file.":
+ log.warning("File not found node: %s file: %s", node, filename)
+ if exitIfFileWasNotFound:
+ closeWithWarning(40, syncFile)
+ except FileNotFoundError:
+ errors += 1
+ log.warning("File not found node: %s file: %s", node, filename)
+ if exitIfFileWasNotFound:
+ closeWithWarning(41, syncFile)
+ except EOFError:
+ errors += 1
+ log.warning("It seems the connection was lost! Try again...")
+ return None
+ except BaseException:
+ errors += 1
+ log.exception(UNEXPECTED_ERROR)
+ return None
+ return 0, 0
+
+
+def download(node, currentIP, files, dns, execution, syncFile, speed):
+ ftp = None
+ size = len(files)
+ global CLOSE
+ sizeInMB = 0
+ downloadTime = 0
+ while not CLOSE and len(files) > 0:
+ if ftp is None:
+ ftp = getFTP(node, currentIP, dns, execution, syncFile)
+ currentIP = None
+ filename = files[0]
+ index = size - len(files) + 1
+ result = downloadFile(ftp, filename, size, index, node, syncFile, speed)
+ if result is None:
+ ftp = None
+ continue
+ sizeInMB += result[0]
+ downloadTime += result[1]
+ files.pop(0)
+ syncFile.write("F-" + filename + '\n')
+ closeFTP(ftp)
+ return node, sizeInMB / downloadTime
+
+
+def waitForFiles(syncFilePath, files, startTime):
+ # wait max. 60 seconds
+ while True:
+ if startTime + 60 < time.time():
+ return False
+ if os.path.isfile(syncFilePath):
+ break
+ log.debug("Wait for file creation")
+ time.sleep(0.1)
+
+ # Read file live
+ with open(syncFilePath, 'r') as syncFileTask:
+ current = []
+ while len(files) > 0:
+ data = syncFileTask.read()
+ if not data:
+ time.sleep(0.3)
+ else:
+ for d in data:
+ if d != "\n":
+ current.append(d)
+ else:
+ text = ''.join(current)
+ current = []
+ if text.startswith("S-"):
+ continue
+ if text == "##FAILURE##":
+ log.debug("Read FAILURE in %s", syncFilePath)
+ myExit(51)
+ if text == "##FINISHED##":
+ log.debug("Read FINISHED in " + syncFilePath + " before all files were found")
+ myExit(52)
+ log.debug("Look for " + text[:2] + " with " + text[2:] + " in " + str(files))
+ if text[:2] == "F-" and text[2:] in files:
+ files.remove(text[2:])
+ if len(files) == 0:
+ return True
+ return len(files) == 0
+
+
+def loadConfig():
+ log.info("Load config")
+ with open(sys.argv[3]) as jsonFile:
+ config = json.load(jsonFile)
+ os.makedirs(config["syncDir"], exist_ok=True)
+ return config
+
+
+def registerSignal(syncFile):
+ signal.signal(signal.SIGINT, lambda signalnum, handler: close(signalnum, syncFile))
+ signal.signal(signal.SIGTERM, lambda signalnum, handler: close(signalnum, syncFile))
+
+
+def registerSignal2():
+ signal.signal(signal.SIGINT, lambda signalnum, handler: myExit(1))
+ signal.signal(signal.SIGTERM, lambda signalnum, handler: myExit(1))
+
+
+def generateSymlinks(symlinks):
+ for s in symlinks:
+ src = s["src"]
+ dst = s["dst"]
+ if clearLocation(src, dst):
+ Path(src[:src.rindex("/")]).mkdir(parents=True, exist_ok=True)
+ try:
+ os.symlink(dst, src)
+ except FileExistsError:
+ log.warning("File exists: %s -> %s", src, dst)
+
+
+def downloadAllData(data, dns, execution, syncFile, speed):
+ global trace
+ throughput = []
+ with ThreadPoolExecutor(max_workers=max(10, len(data))) as executor:
+ futures = []
+ for d in data:
+ files = d["files"]
+ node = d["node"]
+ currentIP = d["currentIP"]
+ futures.append(executor.submit(download, node, currentIP, files, dns, execution, syncFile, speed))
+ lastNum = -1
+ while len(futures) > 0:
+ if lastNum != len(futures):
+ log.info("Wait for %d threads to finish", len(futures))
+ lastNum = len(futures)
+ for f in futures[:]:
+ if f.done():
+ throughput.append(f.result())
+ futures.remove(f)
+ sleep(0.1)
+ trace["scheduler_init_throughput"] = "\"" + ",".join("{}:{:.3f}".format(*x) for x in throughput) + "\""
+
+
+def waitForDependingTasks(waitForFilesOfTask, startTime, syncDir):
+ # Now check for files of other tasks
+ for waitForTask in waitForFilesOfTask:
+ waitForFilesSet = set(waitForFilesOfTask[waitForTask])
+ if not waitForFiles(syncDir + waitForTask, waitForFilesSet, startTime):
+ log.error(syncDir + waitForTask + " was not successful")
+ myExit(200)
+
+
+def writeTrace(dataMap):
+ if sys.argv[1] == 'true':
+ global errors
+ if len(dataMap) == 0 or errors > 0:
+ return
+ with open(traceFilePath, "a") as traceFile:
+ for d in dataMap:
+ traceFile.write(d + "=" + str(dataMap[d]) + "\n")
+ traceFile.write("scheduler_init_errors=" + str(errors) + "\n")
+
+
+def finishedDownload(dns, execution, taskname):
+ try:
+ dns = dns + "downloadtask/" + execution
+ log.info("Request: %s", dns)
+ urllib.request.urlopen(dns, taskname.encode("utf-8"))
+ except BaseException as err:
+ log.exception(err)
+ myExit(100)
+
+
+def run():
+ global trace
+ startTime = time.time()
+ log.info("Start to setup the environment")
+ config = loadConfig()
+
+ dns = config["dns"]
+ execution = config["execution"]
+ data = config["data"]
+ symlinks = config["symlinks"]
+ taskname = config["hash"]
+
+ with open(config["syncDir"] + config["hash"], 'w') as syncFile:
+ registerSignal(syncFile)
+ syncFile.write('##STARTED##\n')
+ syncFile.flush()
+ startTimeSymlinks = time.time()
+ generateSymlinks(symlinks)
+ trace["scheduler_init_symlinks_runtime"] = int((time.time() - startTimeSymlinks) * 1000)
+ syncFile.write('##SYMLINKS##\n')
+ syncFile.flush()
+ startTimeDownload = time.time()
+ downloadAllData(data, dns, execution, syncFile, config["speed"])
+ trace["scheduler_init_download_runtime"] = int((time.time() - startTimeDownload) * 1000)
+ if CLOSE:
+ log.debug("Closed with code %s", str(EXIT))
+ exit(EXIT)
+ log.info("Finished Download")
+ syncFile.write('##FINISHED##\n')
+ registerSignal2()
+
+ # finishedDownload(dns, execution, taskname)
+
+ # startTimeDependingTasks = time.time()
+ # waitForDependingTasks(config["waitForFilesOfTask"], startTime, config["syncDir"])
+ # trace["scheduler_init_depending_tasks_runtime"] = int((time.time() - startTimeDependingTasks) * 1000)
+ # log.info("Waited for all tasks")
+
+ # runtime = int((time.time() - startTime) * 1000)
+ # trace["scheduler_init_runtime"] = runtime
+ # writeTrace(trace)
+
+
+if __name__ == '__main__':
+ run()
diff --git a/daemons/ftp/vsftpd.conf b/daemons/ftp/vsftpd.conf
new file mode 100644
index 00000000..d14bf2df
--- /dev/null
+++ b/daemons/ftp/vsftpd.conf
@@ -0,0 +1,164 @@
+# Example config file /etc/vsftpd.conf
+#
+# The default compiled in settings are fairly paranoid. This sample file
+# loosens things up a bit, to make the ftp daemon more usable.
+# Please see vsftpd.conf.5 for all compiled in defaults.
+#
+# READ THIS: This example file is NOT an exhaustive list of vsftpd options.
+# Please read the vsftpd.conf.5 manual page to get a full idea of vsftpd's
+# capabilities.
+#
+#
+# Run standalone? vsftpd can run either from an inetd or as a standalone
+# daemon started from an initscript.
+listen=YES
+#
+# This directive enables listening on IPv6 sockets. By default, listening
+# on the IPv6 "any" address (::) will accept connections from both IPv6
+# and IPv4 clients. It is not necessary to listen on *both* IPv4 and IPv6
+# sockets. If you want that (perhaps because you want to listen on specific
+# addresses) then you must run two copies of vsftpd with two configuration
+# files.
+listen_ipv6=NO
+#
+# Allow anonymous FTP? (Disabled by default).
+anonymous_enable=YES
+#
+# Uncomment this to allow local users to log in.
+local_enable=YES
+#
+# Uncomment this to enable any form of FTP write command.
+write_enable=YES
+#
+# Default umask for local users is 077. You may wish to change this to 022,
+# if your users expect that (022 is used by most other ftpd's)
+#local_umask=022
+#
+# Uncomment this to allow the anonymous FTP user to upload files. This only
+# has an effect if the above global write enable is activated. Also, you will
+# obviously need to create a directory writable by the FTP user.
+anon_upload_enable=NO
+#
+# Uncomment this if you want the anonymous FTP user to be able to create
+# new directories.
+anon_mkdir_write_enable=NO
+#
+anon_other_write_enable=NO
+#
+# Activate directory messages - messages given to remote users when they
+# go into a certain directory.
+dirmessage_enable=YES
+#
+# If enabled, vsftpd will display directory listings with the time
+# in your local time zone. The default is to display GMT. The
+# times returned by the MDTM FTP command are also affected by this
+# option.
+use_localtime=YES
+#
+# Activate logging of uploads/downloads.
+xferlog_enable=YES
+#
+# Make sure PORT transfer connections originate from port 20 (ftp-data).
+connect_from_port_20=NO
+#
+# If you want, you can arrange for uploaded anonymous files to be owned by
+# a different user. Note! Using "root" for uploaded files is not
+# recommended!
+#chown_uploads=YES
+#chown_username=whoever
+#
+# You may override where the log file goes if you like. The default is shown
+# below.
+#xferlog_file=/var/log/vsftpd.log
+#
+# If you want, you can have your log file in standard ftpd xferlog format.
+# Note that the default log file location is /var/log/xferlog in this case.
+#xferlog_std_format=YES
+#
+# You may change the default value for timing out an idle session.
+#idle_session_timeout=600
+#
+# You may change the default value for timing out a data connection.
+#data_connection_timeout=120
+#
+# It is recommended that you define on your system a unique user which the
+# ftp server can use as a totally isolated and unprivileged user.
+#nopriv_user=ftpsecure
+#
+# Enable this and the server will recognise asynchronous ABOR requests. Not
+# recommended for security (the code is non-trivial). Not enabling it,
+# however, may confuse older FTP clients.
+#async_abor_enable=YES
+#
+# By default the server will pretend to allow ASCII mode but in fact ignore
+# the request. Turn on the below options to have the server actually do ASCII
+# mangling on files when in ASCII mode.
+# Beware that on some FTP servers, ASCII support allows a denial of service
+# attack (DoS) via the command "SIZE /big/file" in ASCII mode. vsftpd
+# predicted this attack and has always been safe, reporting the size of the
+# raw file.
+# ASCII mangling is a horrible feature of the protocol.
+#ascii_upload_enable=YES
+#ascii_download_enable=YES
+#
+# You may fully customise the login banner string:
+#ftpd_banner=Welcome to blah FTP service.
+#
+# You may specify a file of disallowed anonymous e-mail addresses. Apparently
+# useful for combatting certain DoS attacks.
+#deny_email_enable=YES
+# (default follows)
+#banned_email_file=/etc/vsftpd.banned_emails
+#
+# You may restrict local users to their home directories. See the FAQ for
+# the possible risks in this before using chroot_local_user or
+# chroot_list_enable below.
+#chroot_local_user=YES
+#
+# You may specify an explicit list of local users to chroot() to their home
+# directory. If chroot_local_user is YES, then this list becomes a list of
+# users to NOT chroot().
+# (Warning! chroot'ing can be very dangerous. If using chroot, make sure that
+# the user does not have write access to the top level directory within the
+# chroot)
+#chroot_local_user=YES
+#chroot_list_enable=YES
+# (default follows)
+#chroot_list_file=/etc/vsftpd.chroot_list
+#
+# You may activate the "-R" option to the builtin ls. This is disabled by
+# default to avoid remote users being able to cause excessive I/O on large
+# sites. However, some broken FTP clients such as "ncftp" and "mirror" assume
+# the presence of the "-R" option, so there is a strong case for enabling it.
+#ls_recurse_enable=YES
+#
+# Customization
+#
+# Some of vsftpd's settings don't fit the filesystem layout by
+# default.
+#
+# This option should be the name of a directory which is empty. Also, the
+# directory should not be writable by the ftp user. This directory is used
+# as a secure chroot() jail at times vsftpd does not require filesystem
+# access.
+secure_chroot_dir=/var/run/vsftpd/empty
+#
+# This string is the name of the PAM service vsftpd will use.
+pam_service_name=ftp
+#
+# This option specifies the location of the RSA certificate to use for SSL
+# encrypted connections.
+rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
+rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
+ssl_enable=NO
+
+anonymous_enable=yes
+anon_root=/
+
+pasv_enable=Yes
+pasv_max_port=10090
+pasv_min_port=11090
+
+#
+# Uncomment this to indicate that vsftpd use a utf8 filesystem.
+#utf8_filesystem=YES
\ No newline at end of file
diff --git a/jenkins-pod.yaml b/jenkins-pod.yaml
new file mode 100644
index 00000000..eb40c2b4
--- /dev/null
+++ b/jenkins-pod.yaml
@@ -0,0 +1,32 @@
+---
+apiVersion: v1
+kind: Pod
+spec:
+ containers:
+ - name: maven
+ image: maven:3.8.3-jdk-11-slim
+ command: ['sleep', '99d']
+ imagePullPolicy: Always
+ tty: true
+
+ - name: hadolint
+ image: hadolint/hadolint:latest-debian
+ imagePullPolicy: Always
+ command:
+ - cat
+ tty: true
+
+ - name: docker
+ image: docker:20.10.12
+ command: ['sleep', '99d']
+ env:
+ - name: DOCKER_HOST
+ value: tcp://localhost:2375
+
+ - name: docker-daemon
+ image: docker:20.10.12-dind
+ securityContext:
+ privileged: true
+ env:
+ - name: DOCKER_TLS_CERTDIR
+ value: ""
diff --git a/pom.xml b/pom.xml
index ed662853..95aeeea9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
org.springframework.boot
spring-boot-starter-parent
- 3.1.5
+ 3.4.3
@@ -36,7 +36,8 @@
- 17
+ 21
+ 21
cws.k8s.scheduler.Main
@@ -45,18 +46,19 @@
io.fabric8
kubernetes-client
- 6.13.1
+ 7.1.0
- org.projectlombok
- lombok
- provided
+ org.javatuples
+ javatuples
+ 1.2
- junit
- junit
+ org.projectlombok
+ lombok
+ provided
@@ -69,66 +71,64 @@
org.springframework.boot
spring-boot-starter-web
- 3.3.1
+ 3.4.3
org.springframework.boot
spring-boot-starter-test
+ 3.4.3
test
- 3.3.1
org.jetbrains
annotations
- 24.0.1
+ 26.0.2
compile
-
- org.junit.vintage
- junit-vintage-engine
- test
-
-
org.apache.commons
commons-collections4
- 4.4
+ 4.5.0-M3
test
commons-net
commons-net
- 3.10.0
+ 3.11.1
-
- org.springdoc
- springdoc-openapi-starter-webmvc-ui
- 2.2.0
-
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-ui
+ 2.8.5
+
ch.qos.logback
logback-core
- 1.5.3
+ 1.5.17
+
com.fasterxml.jackson.core
jackson-databind
+ 2.18.3
com.fasterxml.jackson.core
jackson-core
+ 2.18.3
com.fasterxml.jackson.core
jackson-annotations
+ 3.0-rc1
@@ -137,11 +137,21 @@
3.6.1
+
+ com.google.ortools
+ ortools-java
+ 9.12.4544
+
+
+
+ org.springframework.boot
+ spring-boot-starter-validation
+
+
-
org.jacoco
jacoco-maven-plugin
- 0.8.7
+ 0.8.12
surefireArgLine
@@ -204,10 +208,7 @@
org.apache.maven.plugins
maven-surefire-plugin
-
-
- ${surefireArgLine} --illegal-access=permit
-
+ 3.5.2
default-test
@@ -232,5 +233,4 @@
-
diff --git a/src/main/java/cws/k8s/scheduler/client/CWSKubernetesClient.java b/src/main/java/cws/k8s/scheduler/client/CWSKubernetesClient.java
index 89f1e2e9..1ea0f75f 100644
--- a/src/main/java/cws/k8s/scheduler/client/CWSKubernetesClient.java
+++ b/src/main/java/cws/k8s/scheduler/client/CWSKubernetesClient.java
@@ -3,18 +3,14 @@
import cws.k8s.scheduler.model.NodeWithAlloc;
import cws.k8s.scheduler.model.PodWithAge;
import cws.k8s.scheduler.model.Task;
+import cws.k8s.scheduler.util.MyExecListner;
import io.fabric8.kubernetes.api.model.*;
-import io.fabric8.kubernetes.client.KubernetesClientException;
-import io.fabric8.kubernetes.client.Watcher;
-import io.fabric8.kubernetes.client.WatcherException;
-import io.fabric8.kubernetes.client.*;
import io.fabric8.kubernetes.client.Config;
-import io.fabric8.kubernetes.client.dsl.MixedOperation;
-import io.fabric8.kubernetes.client.dsl.NonNamespaceOperation;
-import io.fabric8.kubernetes.client.dsl.PodResource;
-import io.fabric8.kubernetes.client.dsl.Resource;
+import io.fabric8.kubernetes.client.*;
+import io.fabric8.kubernetes.client.dsl.*;
import lombok.extern.slf4j.Slf4j;
+import java.io.ByteArrayOutputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
@@ -24,7 +20,7 @@ public class CWSKubernetesClient {
private final KubernetesClient client;
- private final Map nodeHolder = new HashMap<>();
+ private final Map nodeHolder= new HashMap<>();
private final List informables = new LinkedList<>();
public CWSKubernetesClient() {
@@ -37,6 +33,23 @@ public CWSKubernetesClient() {
this.nodes().watch( new NodeWatcher( this ) );
}
+ public Pod getPodByIp( String ip ) {
+ if ( ip == null ) {
+ throw new IllegalArgumentException("IP cannot be null");
+ }
+ return this.pods()
+ .inAnyNamespace()
+ .list()
+ .getItems()
+ .parallelStream()
+ .filter( pod -> ip.equals( pod.getStatus().getPodIP() ) )
+ .findFirst()
+ .orElseGet( () -> {
+ log.warn("No Pod found for IP: {}", ip);
+ return null;
+ });
+ }
+
public NonNamespaceOperation> nodes() {
return client.nodes();
}
@@ -137,6 +150,49 @@ public BigDecimal getMemoryOfNode(NodeWithAlloc node ){
return Quantity.getAmountInBytes(memory);
}
+ private void forceDeletePod( Pod pod ) {
+ this.pods()
+ .inNamespace(pod.getMetadata().getNamespace())
+ .withName(pod.getMetadata().getName())
+ .withGracePeriod(0)
+ .withPropagationPolicy( DeletionPropagation.BACKGROUND )
+ .delete();
+ }
+
+ private void createPod( Pod pod ) {
+ this.pods()
+ .inNamespace(pod.getMetadata().getNamespace())
+ .resource( pod )
+ .create();
+ }
+
+ public void assignPodToNodeAndRemoveInit( PodWithAge pod, String node ) {
+ if ( pod.getSpec().getInitContainers().size() > 0 ) {
+ pod.getSpec().getInitContainers().remove( 0 );
+ }
+ pod.getMetadata().setResourceVersion( null );
+ pod.getMetadata().setManagedFields( null );
+ pod.getSpec().setNodeName( node );
+
+ forceDeletePod( pod );
+ createPod( pod );
+ }
+
+ public void execCommand( String podName, String namespace, String[] command, MyExecListner listener ){
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayOutputStream error = new ByteArrayOutputStream();
+ final ExecWatch exec = this.pods()
+ .inNamespace( namespace )
+ .withName( podName )
+ .writingOutput( out )
+ .writingError( error )
+ .usingListener( listener )
+ .exec( command );
+ listener.setExec( exec );
+ listener.setError( error );
+ listener.setOut( out );
+ }
+
static class NodeWatcher implements Watcher{
private final CWSKubernetesClient kubernetesClient;
@@ -146,7 +202,7 @@ public NodeWatcher(CWSKubernetesClient kubernetesClient) {
}
@Override
- public void eventReceived(Action action, Node node) {
+ public void eventReceived( Watcher.Action action, Node node) {
boolean change = false;
NodeWithAlloc processedNode = null;
switch (action) {
@@ -269,7 +325,7 @@ public boolean featureGateActive( String featureGate ){
* It will create a patch for the memory limits and request values and submit it
* to the cluster.
* Moreover, it updates the task with the new pod.
- *
+ *
* @param t the task to be patched
* @return false if patching failed because of InPlacePodVerticalScaling
*/
diff --git a/src/main/java/cws/k8s/scheduler/dag/Origin.java b/src/main/java/cws/k8s/scheduler/dag/Origin.java
index b9b64051..3536b2d1 100644
--- a/src/main/java/cws/k8s/scheduler/dag/Origin.java
+++ b/src/main/java/cws/k8s/scheduler/dag/Origin.java
@@ -19,7 +19,7 @@ public Type getType() {
@Override
public void addInbound(Edge e) {
- throw new IllegalStateException("Cannot add an inbound to an Origin");
+ throw new IllegalStateException("Cannot add an Edge(uid: " + e.getUid() + "; " + e.getFrom().getUid() + " -> " + e.getTo().getUid() + ") inbound to an Origin (uid: " + getUid() + ")");
}
public Set getAncestors() {
diff --git a/src/main/java/cws/k8s/scheduler/dag/Process.java b/src/main/java/cws/k8s/scheduler/dag/Process.java
index 3e03817b..18da2788 100644
--- a/src/main/java/cws/k8s/scheduler/dag/Process.java
+++ b/src/main/java/cws/k8s/scheduler/dag/Process.java
@@ -19,10 +19,6 @@ public int getSuccessfullyFinished() {
return successfullyFinished.get();
}
- public int getFailed() {
- return failed.get();
- }
-
public void incrementSuccessfullyFinished() {
successfullyFinished.incrementAndGet();
}
diff --git a/src/main/java/cws/k8s/scheduler/model/DateParser.java b/src/main/java/cws/k8s/scheduler/model/DateParser.java
new file mode 100644
index 00000000..845bfda8
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/DateParser.java
@@ -0,0 +1,32 @@
+package cws.k8s.scheduler.model;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+import java.text.SimpleDateFormat;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public final class DateParser {
+
+ public static Long millisFromString( String date ) {
+ if( date == null || date.isEmpty() || date.equals("-") || date.equals("w") ) {
+ return null;
+ }
+ try {
+ if (Character.isLetter(date.charAt(0))) {
+ // if ls was used, date has the format "Nov 2 08:49:30 2021"
+ return new SimpleDateFormat("MMM dd HH:mm:ss yyyy").parse(date).getTime();
+ } else {
+ // if stat was used, date has the format "2021-11-02 08:49:30.955691861 +0000"
+ String[] parts = date.split(" ");
+ parts[1] = parts[1].substring(0, 12);
+ // parts[1] now has milliseconds as smallest units e.g. "08:49:30.955"
+ String shortenedDate = String.join(" ", parts);
+ return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS Z").parse(shortenedDate).getTime();
+ }
+ } catch ( Exception e ){
+ return null;
+ }
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/FileHolder.java b/src/main/java/cws/k8s/scheduler/model/FileHolder.java
index e7d45a5a..6b7ff6f9 100644
--- a/src/main/java/cws/k8s/scheduler/model/FileHolder.java
+++ b/src/main/java/cws/k8s/scheduler/model/FileHolder.java
@@ -6,7 +6,7 @@
import lombok.ToString;
@ToString( exclude = {"stageName", "storePath"})
-@NoArgsConstructor(access = AccessLevel.NONE)
+@NoArgsConstructor(access = AccessLevel.NONE, force = true)
@RequiredArgsConstructor
public class FileHolder {
diff --git a/src/main/java/cws/k8s/scheduler/model/ImmutableRequirements.java b/src/main/java/cws/k8s/scheduler/model/ImmutableRequirements.java
new file mode 100644
index 00000000..540187db
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/ImmutableRequirements.java
@@ -0,0 +1,42 @@
+package cws.k8s.scheduler.model;
+
+import java.math.BigDecimal;
+
+public class ImmutableRequirements extends Requirements {
+
+ public static final Requirements ZERO = new ImmutableRequirements();
+
+ private static final String ERROR_MESSAGE = "ImmutableRequirements cannot be modified";
+
+ public ImmutableRequirements( BigDecimal cpu, BigDecimal ram ) {
+ super( cpu, ram );
+ }
+
+ public ImmutableRequirements(){
+ super();
+ }
+
+ public ImmutableRequirements( Requirements requirements ){
+ super( requirements.getCpu(), requirements.getRam() );
+ }
+
+ @Override
+ public Requirements addCPUtoThis( BigDecimal cpu ) {
+ throw new IllegalStateException( ERROR_MESSAGE );
+ }
+
+ @Override
+ public Requirements addRAMtoThis( BigDecimal ram ) {
+ throw new IllegalStateException( ERROR_MESSAGE );
+ }
+
+ @Override
+ public Requirements addToThis( Requirements requirements ) {
+ throw new IllegalStateException( ERROR_MESSAGE );
+ }
+
+ @Override
+ public Requirements subFromThis( Requirements requirements ) {
+ throw new IllegalStateException( ERROR_MESSAGE );
+ }
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/InputFileCollector.java b/src/main/java/cws/k8s/scheduler/model/InputFileCollector.java
new file mode 100644
index 00000000..c6d4943b
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/InputFileCollector.java
@@ -0,0 +1,91 @@
+package cws.k8s.scheduler.model;
+
+import cws.k8s.scheduler.model.location.Location;
+import cws.k8s.scheduler.model.location.hierachy.*;
+import cws.k8s.scheduler.model.taskinputs.PathFileLocationTriple;
+import cws.k8s.scheduler.model.taskinputs.SymlinkInput;
+import cws.k8s.scheduler.model.taskinputs.TaskInputs;
+import cws.k8s.scheduler.util.Tuple;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.file.Path;
+import java.util.*;
+
+@Slf4j
+@RequiredArgsConstructor
+public class InputFileCollector {
+
+ private final HierarchyWrapper hierarchyWrapper;
+
+ private void processNext(
+ final LinkedList> toProcess,
+ final List symlinks,
+ final List files,
+ final Set excludedLocations,
+ final Task task
+ ) throws NoAlignmentFoundException {
+ final Tuple tuple = toProcess.removeLast();
+ final HierarchyFile file = tuple.getA();
+ if( file == null ) {
+ return;
+ }
+ final Path path = tuple.getB();
+ if ( file.isSymlink() ){
+ final Path linkTo = ((LinkHierarchyFile) file).getDst();
+ symlinks.add( new SymlinkInput( path, linkTo ) );
+ toProcess.push( new Tuple<>( hierarchyWrapper.getFile( linkTo ), linkTo ) );
+ } else if( file.isDirectory() ){
+ final Map allChildren = ((Folder) file).getAllChildren(path);
+ for (Map.Entry pathAbstractFileEntry : allChildren.entrySet()) {
+ toProcess.push( new Tuple<>( pathAbstractFileEntry.getValue(), pathAbstractFileEntry.getKey() ) );
+ }
+ } else {
+ final RealHierarchyFile realFile = (RealHierarchyFile) file;
+ realFile.requestedByTask();
+ try {
+ final RealHierarchyFile.MatchingLocationsPair filesForTask = realFile.getFilesForTask(task);
+ if ( filesForTask.getExcludedNodes() != null ) {
+ excludedLocations.addAll(filesForTask.getExcludedNodes());
+ }
+ files.add( new PathFileLocationTriple( path, realFile, filesForTask.getMatchingLocations()) );
+ } catch ( NoAlignmentFoundException e ){
+ log.error( "No alignment for task: " + task.getConfig().getName() + " path: " + path, e );
+ throw e;
+ }
+ }
+ }
+
+ public TaskInputs getInputsOfTask( Task task, int numberNode ) throws NoAlignmentFoundException {
+
+ final List> fileInputs = task.getConfig().getInputs().fileInputs;
+ final LinkedList> toProcess = filterFilesToProcess( fileInputs );
+
+ final List symlinks = new ArrayList<>( fileInputs.size() );
+ final List files = new ArrayList<>( fileInputs.size() );
+ final Set excludedLocations = new HashSet<>();
+
+ while ( !toProcess.isEmpty() && excludedLocations.size() < numberNode ){
+ processNext( toProcess, symlinks, files, excludedLocations, task );
+ }
+
+ if( excludedLocations.size() == numberNode ) {
+ return null;
+ }
+
+ return new TaskInputs( symlinks, files, excludedLocations );
+
+ }
+
+ private LinkedList> filterFilesToProcess(List> fileInputs ){
+ final LinkedList> toProcess = new LinkedList<>();
+ for ( InputParam fileInput : fileInputs) {
+ final Path path = Path.of(fileInput.value.sourceObj);
+ if ( this.hierarchyWrapper.isInScope( path ) ){
+ toProcess.add( new Tuple<>(hierarchyWrapper.getFile( path ), path) );
+ }
+ }
+ return toProcess;
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/NodeWithAlloc.java b/src/main/java/cws/k8s/scheduler/model/NodeWithAlloc.java
index 734d808e..c8b01f56 100644
--- a/src/main/java/cws/k8s/scheduler/model/NodeWithAlloc.java
+++ b/src/main/java/cws/k8s/scheduler/model/NodeWithAlloc.java
@@ -1,6 +1,7 @@
package cws.k8s.scheduler.model;
import cws.k8s.scheduler.client.CWSKubernetesClient;
+import cws.k8s.scheduler.model.location.NodeLocation;
import io.fabric8.kubernetes.api.model.Node;
import io.fabric8.kubernetes.api.model.ObjectMeta;
import io.fabric8.kubernetes.api.model.Pod;
@@ -10,8 +11,7 @@
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
-import java.util.HashMap;
-import java.util.Map;
+import java.util.*;
@Getter
@Slf4j
@@ -25,10 +25,16 @@ public class NodeWithAlloc extends Node implements Comparable {
private final Map assignedPods;
+ private final List startingTaskCopyingData = new LinkedList<>();
+
+ @Getter
+ private final NodeLocation nodeLocation;
+
public NodeWithAlloc( String name ) {
this.kubernetesClient = null;
this.maxResources = null;
this.assignedPods = null;
+ this.nodeLocation = null;
this.setMetadata( new ObjectMeta() );
this.getMetadata().setName( name );
}
@@ -41,6 +47,8 @@ public NodeWithAlloc( Node node, CWSKubernetesClient kubernetesClient ) {
assignedPods = new HashMap<>();
+ this.nodeLocation = NodeLocation.getLocation( node );
+
}
@@ -98,20 +106,46 @@ private void setNodeData( Node node, boolean isCreate ) {
public void addPod( PodWithAge pod ) {
Requirements request = pod.getRequest();
synchronized (assignedPods) {
- assignedPods.put( pod.getMetadata().getUid(), request );
+ assignedPods.put( getPodInternalId( pod ) , request );
+ }
+ }
+
+ private String getPodInternalId( Pod pod ) {
+ return pod.getMetadata().getNamespace() + "||" + pod.getMetadata().getName();
+ }
+
+ private void removeStartingTaskCopyingDataByUid( String uid ) {
+ synchronized ( startingTaskCopyingData ) {
+ final Iterator iterator = startingTaskCopyingData.iterator();
+ while ( iterator.hasNext() ) {
+ final PodWithAge podWithAge = iterator.next();
+ if ( podWithAge.getMetadata().getUid().equals( uid ) ) {
+ iterator.remove();
+ break;
+ }
+ }
}
}
public boolean removePod( Pod pod ){
synchronized (assignedPods) {
- return assignedPods.remove( pod.getMetadata().getUid() ) != null;
+ return assignedPods.remove( getPodInternalId( pod ) ) != null;
}
}
+ public void startingTaskCopyingDataFinished( Task task ) {
+ final String uid = task.getPod().getMetadata().getUid();
+ removeStartingTaskCopyingDataByUid( uid );
+ }
+
public int getRunningPods(){
return assignedPods.size();
}
+ public int getStartingPods(){
+ return startingTaskCopyingData.size();
+ }
+
/**
* @return max(Requested by all and currently used )
*/
@@ -173,4 +207,25 @@ public int hashCode() {
public boolean isReady(){
return Readiness.isNodeReady(this);
}
+
+ public boolean affinitiesMatch( PodWithAge pod ){
+
+ final boolean nodeCouldRunThisPod = this.getMaxResources().higherOrEquals( pod.getRequest() );
+ if ( !nodeCouldRunThisPod ){
+ return false;
+ }
+
+ final Map podsNodeSelector = pod.getSpec().getNodeSelector();
+ final Map nodesLabels = this.getMetadata().getLabels();
+ if ( podsNodeSelector == null || podsNodeSelector.isEmpty() ) {
+ return true;
+ }
+ //cannot be fulfilled if podsNodeSelector is not empty
+ if ( nodesLabels == null || nodesLabels.isEmpty() ) {
+ return false;
+ }
+
+ return nodesLabels.entrySet().containsAll( podsNodeSelector.entrySet() );
+ }
+
}
diff --git a/src/main/java/cws/k8s/scheduler/model/PodWithAge.java b/src/main/java/cws/k8s/scheduler/model/PodWithAge.java
index fb989052..e111d92a 100644
--- a/src/main/java/cws/k8s/scheduler/model/PodWithAge.java
+++ b/src/main/java/cws/k8s/scheduler/model/PodWithAge.java
@@ -1,7 +1,8 @@
package cws.k8s.scheduler.model;
import cws.k8s.scheduler.util.PodPhase;
-import io.fabric8.kubernetes.api.model.*;
+import io.fabric8.kubernetes.api.model.Pod;
+import io.fabric8.kubernetes.api.model.Quantity;
import lombok.Getter;
import lombok.Setter;
@@ -13,21 +14,13 @@ public class PodWithAge extends Pod {
@Setter
private BigDecimal age;
- public PodWithAge(ObjectMeta metadata, PodSpec spec, PodStatus status) {
- super("v1", "Pod", metadata, spec, status);
- this.age = BigDecimal.ZERO;
- }
- public PodWithAge() {
- super("v1", "Pod", null, null, null);
- this.age = BigDecimal.ZERO;
- }
public PodWithAge(Pod pod) {
super(pod.getApiVersion(), pod.getKind(), pod.getMetadata(), pod.getSpec(), pod.getStatus());
this.age = BigDecimal.ZERO;
}
public Requirements getRequest(){
- return this
+ return new ImmutableRequirements( this
.getSpec().getContainers().stream()
.filter( x -> x.getResources() != null
&& x.getResources().getRequests() != null )
@@ -36,7 +29,7 @@ public Requirements getRequest(){
x.getResources().getRequests().get("cpu") == null ? null : Quantity.getAmountInBytes(x.getResources().getRequests().get("cpu")),
x.getResources().getRequests().get("memory") == null ? null : Quantity.getAmountInBytes(x.getResources().getRequests().get("memory"))
)
- ).reduce( new Requirements(), Requirements::addToThis );
+ ).reduce( new Requirements(), Requirements::addToThis ) );
}
public String getName(){
diff --git a/src/main/java/cws/k8s/scheduler/model/Requirements.java b/src/main/java/cws/k8s/scheduler/model/Requirements.java
index 2ba931b4..1fcb57cc 100644
--- a/src/main/java/cws/k8s/scheduler/model/Requirements.java
+++ b/src/main/java/cws/k8s/scheduler/model/Requirements.java
@@ -7,7 +7,7 @@
import static cws.k8s.scheduler.util.Formater.formatBytes;
-public class Requirements implements Serializable {
+public class Requirements implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
@@ -23,6 +23,16 @@ public Requirements( BigDecimal cpu, BigDecimal ram ) {
this.ram = ram == null ? BigDecimal.ZERO : ram;
}
+ /**
+ * Basically used for testing
+ * @param cpu
+ * @param ram
+ */
+ public Requirements( int cpu, int ram ) {
+ this.cpu = BigDecimal.valueOf(cpu);
+ this.ram = BigDecimal.valueOf(ram);
+ }
+
public Requirements(){
this( BigDecimal.ZERO, BigDecimal.ZERO );
}
@@ -33,6 +43,13 @@ public Requirements addToThis( Requirements requirements ){
return this;
}
+ public Requirements add( Requirements requirements ){
+ return new Requirements(
+ this.cpu.add(requirements.cpu),
+ this.ram.add(requirements.ram)
+ );
+ }
+
public Requirements addRAMtoThis( BigDecimal ram ){
this.ram = this.ram.add( ram );
return this;
@@ -56,6 +73,19 @@ public Requirements sub( Requirements requirements ){
);
}
+ public Requirements multiply( BigDecimal factor ){
+ return new Requirements(
+ this.cpu.multiply(factor),
+ this.ram.multiply(factor)
+ );
+ }
+
+ public Requirements multiplyToThis( BigDecimal factor ){
+ this.cpu = this.cpu.multiply(factor);
+ this.ram = this.ram.multiply(factor);
+ return this;
+ }
+
public boolean higherOrEquals( Requirements requirements ){
return this.cpu.compareTo( requirements.cpu ) >= 0
&& this.ram.compareTo( requirements.ram ) >= 0;
@@ -84,4 +114,29 @@ public int hashCode() {
result = 31 * result + (getRam() != null ? getRam().hashCode() : 0);
return result;
}
+
+ /**
+ * Always returns a mutable copy of this object
+ * @return
+ */
+ @Override
+ public Requirements clone() {
+ return new Requirements( this.cpu, this.ram );
+ }
+
+ public boolean smaller( Requirements request ) {
+ return this.cpu.compareTo( request.cpu ) < 0
+ && this.ram.compareTo( request.ram ) < 0;
+ }
+
+ public boolean smallerEquals( Requirements request ) {
+ return this.cpu.compareTo( request.cpu ) <= 0
+ && this.ram.compareTo( request.ram ) <= 0;
+ }
+
+ public boolean atLeastOneBigger( Requirements request ) {
+ return this.cpu.compareTo( request.cpu ) > 0
+ || this.ram.compareTo( request.ram ) > 0;
+ }
+
}
diff --git a/src/main/java/cws/k8s/scheduler/model/SchedulerConfig.java b/src/main/java/cws/k8s/scheduler/model/SchedulerConfig.java
index d55233f7..743f0965 100644
--- a/src/main/java/cws/k8s/scheduler/model/SchedulerConfig.java
+++ b/src/main/java/cws/k8s/scheduler/model/SchedulerConfig.java
@@ -12,18 +12,40 @@
@NoArgsConstructor(access = AccessLevel.PRIVATE,force = true)
public class SchedulerConfig {
+ public final List localClaims;
public final List volumeClaims;
public final String workDir;
public final String dns;
+ public final String copyStrategy;
+ public final boolean locationAware;
public final boolean traceEnabled;
public final String namespace;
public final String costFunction;
public final String strategy;
+
+ public final Integer maxCopyTasksPerNode;
+
+ public final Integer maxWaitingCopyTasksPerNode;
+ public final Integer maxHeldCopyTaskReady;
+ public final Integer prioPhaseThree;
+
public final Map additional;
public final String memoryPredictor;
public final Long maxMemory;
public final Long minMemory;
+ @ToString
+ public static class LocalClaim {
+ public final String mountPath;
+ public final String hostPath;
+
+ private LocalClaim(){
+ this.mountPath = null;
+ this.hostPath = null;
+ }
+
+ }
+
@ToString
@NoArgsConstructor(access = AccessLevel.PRIVATE,force = true)
public static class VolumeClaim {
diff --git a/src/main/java/cws/k8s/scheduler/model/Task.java b/src/main/java/cws/k8s/scheduler/model/Task.java
index 0b934c5c..b3a832ba 100644
--- a/src/main/java/cws/k8s/scheduler/model/Task.java
+++ b/src/main/java/cws/k8s/scheduler/model/Task.java
@@ -2,15 +2,23 @@
import cws.k8s.scheduler.dag.DAG;
import cws.k8s.scheduler.dag.Process;
+import cws.k8s.scheduler.model.cluster.OutputFiles;
+import cws.k8s.scheduler.model.location.hierachy.HierarchyWrapper;
+import cws.k8s.scheduler.model.location.hierachy.LocationWrapper;
import cws.k8s.scheduler.model.tracing.TraceRecord;
import cws.k8s.scheduler.util.Batch;
+import cws.k8s.scheduler.util.copying.CurrentlyCopyingOnNode;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.math.BigDecimal;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Stream;
@Slf4j
public class Task {
@@ -28,6 +36,14 @@ public class Task {
@Getter
private final Process process;
+ @Getter
+ @Setter
+ private List inputFiles;
+
+ @Getter
+ @Setter
+ private List< TaskInputFileLocationWrapper > copiedFiles;
+
@Getter
private PodWithAge pod = null;
@@ -39,6 +55,10 @@ public class Task {
@Setter
private Batch batch;
+ @Getter
+ @Setter
+ private CurrentlyCopyingOnNode copyingToNode;
+
@Getter
private final TraceRecord traceRecord = new TraceRecord();
@@ -49,7 +69,6 @@ public class Task {
private final Requirements oldRequirements;
- @Getter
private Requirements planedRequirements;
@Getter
@@ -58,11 +77,39 @@ public class Task {
@Getter
private long cpuPredictionVersion = -1;
+ private final AtomicInteger copyTaskId = new AtomicInteger(0);
+
+ private final HierarchyWrapper hierarchyWrapper;
+
+ @Getter
+ @Setter
+ private OutputFiles outputFiles;
+
public Task( TaskConfig config, DAG dag ) {
+ this( config, dag, null );
+ }
+
+ public Task( TaskConfig config, DAG dag, HierarchyWrapper hierarchyWrapper ) {
this.config = config;
oldRequirements = new Requirements( BigDecimal.valueOf(config.getCpus()), BigDecimal.valueOf(config.getMemoryInBytes()) );
planedRequirements = new Requirements( BigDecimal.valueOf(config.getCpus()), BigDecimal.valueOf(config.getMemoryInBytes()) );
this.process = dag.getByProcess( config.getTask() );
+ this.hierarchyWrapper = hierarchyWrapper;
+ }
+
+ /**
+ * Constructor for inheritance
+ */
+ protected Task( TaskConfig config, Process process ){
+ this.config = config;
+ oldRequirements = new Requirements( BigDecimal.valueOf(config.getCpus()), BigDecimal.valueOf(config.getMemoryInBytes()) );
+ planedRequirements = new Requirements( BigDecimal.valueOf(config.getCpus()), BigDecimal.valueOf(config.getMemoryInBytes()) );
+ this.process = process;
+ this.hierarchyWrapper = null;
+ }
+
+ public int getCurrentCopyTaskId() {
+ return copyTaskId.getAndIncrement();
}
public synchronized void setTaskMetrics( TaskMetrics taskMetrics ){
@@ -104,8 +151,16 @@ public void submitted(){
traceRecord.setSchedulerTimeInQueue( System.currentTimeMillis() - timeAddedToQueue );
}
+ public Set getOutLabel(){
+ return config.getOutLabel();
+ }
+
private long inputSize = -1;
+ /**
+ * Calculates the size of all input files in bytes in the shared filesystem.
+ * @return The sum of all input files in bytes.
+ */
public long getInputSize(){
if ( config.getInputSize() != null ) {
return config.getInputSize();
@@ -113,10 +168,18 @@ public long getInputSize(){
synchronized ( this ) {
if ( inputSize == -1 ) {
//calculate
- inputSize = getConfig()
+ Stream> inputParamStream = getConfig()
.getInputs()
.fileInputs
- .parallelStream()
+ .parallelStream();
+ //If LA Scheduling, filter out files that are not in sharedFS
+ if ( hierarchyWrapper != null ) {
+ inputParamStream = inputParamStream.filter( x -> {
+ final Path path = Path.of( x.value.sourceObj );
+ return !hierarchyWrapper.isInScope( path );
+ } );
+ }
+ inputSize = inputParamStream
.mapToLong( input -> new File(input.value.sourceObj).length() )
.sum();
}
@@ -130,10 +193,15 @@ public String toString() {
return "Task{" +
"state=" + state +
", pod=" + (pod == null ? "--" : pod.getMetadata().getName()) +
+ ", node='" + (node != null ? node.getNodeLocation().getIdentifier() : "--") + '\'' +
", workDir='" + getWorkingDir() + '\'' +
'}';
}
+ public Requirements getPlanedRequirements() {
+ return planedRequirements.clone();
+ }
+
public long getNewMemoryRequest(){
return getPlanedRequirements().getRam().longValue();
}
diff --git a/src/main/java/cws/k8s/scheduler/model/TaskConfig.java b/src/main/java/cws/k8s/scheduler/model/TaskConfig.java
index f3e1ec44..9619663f 100644
--- a/src/main/java/cws/k8s/scheduler/model/TaskConfig.java
+++ b/src/main/java/cws/k8s/scheduler/model/TaskConfig.java
@@ -8,6 +8,7 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Set;
@Getter
@ToString
@@ -20,6 +21,7 @@ public class TaskConfig {
private final String runName;
private final float cpus;
private final long memoryInBytes;
+ private final Set outLabel;
private final String workDir;
private final int repetition;
private final Long inputSize;
@@ -40,6 +42,21 @@ public TaskConfig(String task) {
this.cpus = 0;
this.memoryInBytes = 0;
this.workDir = null;
+ this.outLabel = null;
+ this.repetition = 0;
+ this.inputSize = null;
+ }
+
+ public TaskConfig(String task, String name, String workDir, String runName ) {
+ this.task = task;
+ this.name = name;
+ this.schedulerParams = null;
+ this.inputs = new TaskInput( null, null, null, new LinkedList<>() );
+ this.runName = runName;
+ this.cpus = 0;
+ this.memoryInBytes = 0;
+ this.workDir = workDir;
+ this.outLabel = null;
this.repetition = 0;
this.inputSize = null;
}
diff --git a/src/main/java/cws/k8s/scheduler/model/TaskInput.java b/src/main/java/cws/k8s/scheduler/model/TaskInput.java
index d0b8b3ab..7aac8b53 100644
--- a/src/main/java/cws/k8s/scheduler/model/TaskInput.java
+++ b/src/main/java/cws/k8s/scheduler/model/TaskInput.java
@@ -27,4 +27,5 @@ public String toString() {
", fileInputs=#" + (fileInputs != null ? fileInputs.size() : 0) +
'}';
}
+
}
diff --git a/src/main/java/cws/k8s/scheduler/model/TaskInputFileLocationWrapper.java b/src/main/java/cws/k8s/scheduler/model/TaskInputFileLocationWrapper.java
new file mode 100644
index 00000000..bbbb1811
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/TaskInputFileLocationWrapper.java
@@ -0,0 +1,24 @@
+package cws.k8s.scheduler.model;
+
+import cws.k8s.scheduler.model.location.hierachy.LocationWrapper;
+import cws.k8s.scheduler.model.location.hierachy.RealHierarchyFile;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public class TaskInputFileLocationWrapper {
+
+ private final String path;
+ private final RealHierarchyFile file;
+ private final LocationWrapper wrapper;
+
+ public void success(){
+ file.addOrUpdateLocation( false, wrapper );
+ }
+
+ public void failure(){
+ file.removeLocation( wrapper );
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/TaskResultParser.java b/src/main/java/cws/k8s/scheduler/model/TaskResultParser.java
new file mode 100644
index 00000000..8edd5fac
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/TaskResultParser.java
@@ -0,0 +1,177 @@
+package cws.k8s.scheduler.model;
+
+import cws.k8s.scheduler.model.location.Location;
+import cws.k8s.scheduler.model.location.hierachy.LocationWrapper;
+import cws.k8s.scheduler.model.outfiles.OutputFile;
+import cws.k8s.scheduler.model.outfiles.PathLocationWrapperPair;
+import cws.k8s.scheduler.model.outfiles.SymlinkOutput;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashSet;
+import java.util.Scanner;
+import java.util.Set;
+import java.util.stream.Stream;
+
+@Slf4j
+public class TaskResultParser {
+
+ static final int VIRTUAL_PATH = 0;
+ static final int FILE_EXISTS = 1;
+ static final int REAL_PATH = 2;
+ static final int SIZE = 3;
+ static final int FILE_TYPE = 4;
+ static final int CREATION_DATE = 5;
+ static final int ACCESS_DATE = 6;
+ static final int MODIFICATION_DATE = 7;
+
+ public String getIndex( File file, int index ){
+ try ( Scanner sc = new Scanner( file ) ) {
+ int i = 0;
+ while( sc.hasNext() && i++ < index ) {
+ sc.nextLine();
+ }
+ if ( sc.hasNext() ) {
+ return sc.nextLine();
+ }
+ } catch (FileNotFoundException e) {
+ log.error( "Cannot read " + file, e);
+ }
+ return null;
+ }
+
+ private String getRootDir( File file, int index ){
+ String data = getIndex( file, index );
+ if ( data == null ) {
+ return null;
+ }
+ return data.split(";")[0];
+ }
+
+ private Long getDateDir(File file ){
+ String data = getIndex( file, 0 );
+ if ( data == null ) {
+ return null;
+ }
+ return Long.parseLong( data );
+ }
+
+ public void processInput( Stream in, final Set inputdata ){
+ in.skip( 2 )
+ .forEach( line -> {
+ String[] data = line.split(";");
+ if( data[ FILE_EXISTS ].equals("0") ) {
+ return;
+ }
+ if ( data[ 3 ].equals("directory") ) {
+ return;
+ }
+ String path = data[ REAL_PATH ].equals("") ? data[ VIRTUAL_PATH ] : data[ REAL_PATH ];
+ inputdata.add( path );
+ });
+ }
+
+ private Set processOutput(
+ final Stream out,
+ final Set inputdata,
+ final Location location,
+ final boolean onlyUpdated,
+ final Task finishedTask,
+ final String outputRootDir,
+ final long initailDate
+ ){
+ final Set newOrUpdated = new HashSet<>();
+ out.skip( 1 )
+ .forEach( line -> {
+ String[] data = line.split(";");
+ if( data[ FILE_EXISTS ].equals("0") && data.length != 8 ) {
+ return;
+ }
+ boolean isSymlink = !data[ REAL_PATH ].equals("");
+ String path = isSymlink ? data[ REAL_PATH ] : data[ VIRTUAL_PATH ];
+ String modificationDate = data[ MODIFICATION_DATE ];
+ if ( "directory".equals( data[ FILE_TYPE ] ) ) {
+ return;
+ }
+ String lockupPath = isSymlink ? path : path.substring( outputRootDir.length() );
+ long modificationDateNano = Long.parseLong( modificationDate );
+ if ( ( !inputdata.contains(lockupPath) && !onlyUpdated )
+ ||
+ modificationDateNano > initailDate )
+ {
+ final LocationWrapper locationWrapper = new LocationWrapper(
+ location,
+ modificationDateNano / (int) 1.0E6,
+ Long.parseLong(data[ SIZE ]),
+ finishedTask
+ );
+ newOrUpdated.add( new PathLocationWrapperPair( Paths.get(path), locationWrapper ) );
+ }
+ if( isSymlink ){
+ newOrUpdated.add( new SymlinkOutput( data[ VIRTUAL_PATH ], path ));
+ }
+ });
+ return newOrUpdated;
+ }
+
+ /**
+ *
+ * @param workdir
+ * @param location
+ * @param onlyUpdated
+ * @param finishedTask
+ * @return A list of all new or updated files
+ */
+ public Set getNewAndUpdatedFiles(
+ final Path workdir,
+ final Location location,
+ final boolean onlyUpdated,
+ Task finishedTask
+ ){
+
+ final Path infile = workdir.resolve(".command.infiles");
+ final Path outfile = workdir.resolve(".command.outfiles");
+ if ( !outfile.toFile().exists() ) {
+ log.error( "Cannot find outfile " + infile );
+ return new HashSet<>();
+ }
+
+ final String taskRootDir = getRootDir( infile.toFile(), 1 );
+ if( taskRootDir == null
+ && (finishedTask.getInputFiles() == null || finishedTask.getInputFiles().isEmpty()) ) {
+ throw new IllegalStateException("taskRootDir is null");
+ }
+
+
+ final String outputRootDir = getRootDir( outfile.toFile(), 0 );
+ //No outputs defined / found
+ if( outputRootDir == null ) {
+ return new HashSet<>();
+ }
+
+ final Set inputdata = new HashSet<>();
+
+
+ try (
+ Stream in = Files.lines(infile);
+ Stream out = Files.lines(outfile)
+ ) {
+
+ processInput( in, inputdata );
+ log.trace( "{}", inputdata );
+ final Long initialDate = getDateDir( infile.toFile() );
+ return processOutput( out, inputdata, location, onlyUpdated, finishedTask, outputRootDir, initialDate );
+
+ } catch (IOException e) {
+ log.error( "Cannot read in/outfile in workdir: " + workdir, e);
+ }
+ return new HashSet<>();
+
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/TaskState.java b/src/main/java/cws/k8s/scheduler/model/TaskState.java
index 04d7b6d7..05f8278a 100644
--- a/src/main/java/cws/k8s/scheduler/model/TaskState.java
+++ b/src/main/java/cws/k8s/scheduler/model/TaskState.java
@@ -16,5 +16,6 @@ public void error (String error){
this.state = State.ERROR;
this.error = error;
}
+
}
diff --git a/src/main/java/cws/k8s/scheduler/model/WriteConfigResult.java b/src/main/java/cws/k8s/scheduler/model/WriteConfigResult.java
new file mode 100644
index 00000000..6f4246c4
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/WriteConfigResult.java
@@ -0,0 +1,20 @@
+package cws.k8s.scheduler.model;
+
+import cws.k8s.scheduler.util.copying.CurrentlyCopyingOnNode;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.List;
+import java.util.Map;
+
+@Getter
+@AllArgsConstructor
+public class WriteConfigResult {
+
+ private final List inputFiles;
+ private final Map waitForTask;
+ private final CurrentlyCopyingOnNode copyingToNode;
+ private final boolean wroteConfig;
+ private boolean copyDataToNode;
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/cluster/CopyTask.java b/src/main/java/cws/k8s/scheduler/model/cluster/CopyTask.java
new file mode 100644
index 00000000..967433ef
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/cluster/CopyTask.java
@@ -0,0 +1,63 @@
+package cws.k8s.scheduler.model.cluster;
+
+import cws.k8s.scheduler.dag.Process;
+import cws.k8s.scheduler.model.NodeWithAlloc;
+import cws.k8s.scheduler.model.Requirements;
+import cws.k8s.scheduler.model.Task;
+import cws.k8s.scheduler.model.TaskConfig;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.file.Path;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Slf4j
+public class CopyTask extends Task {
+
+ private static final Process COPY_PROCESS = new Process( "Copy", Integer.MAX_VALUE );
+ private final Task task;
+ private final NodeWithAlloc node;
+ private final List labelCounts;
+
+ protected CopyTask( Task task, NodeWithAlloc node, List labelCounts ) {
+ super( new TaskConfig("Copy","Copy-" + task.getConfig().getName() + " -> " + node.getNodeLocation()
+ + " (" + labelCounts.stream().map( LabelCount::getLabel ).collect( Collectors.joining(",")) + ")" ,
+ buildCopyTaskFolder(task), task.getConfig().getRunName() ), COPY_PROCESS );
+ this.task = task;
+ this.node = node;
+ this.labelCounts = labelCounts;
+ }
+
+ private static String buildCopyTaskFolder( Task task ) {
+ final Path path = Path.of( task.getConfig().getWorkDir() );
+ Path p = Path.of(
+ path.getParent().getParent().toString(),
+ "copy",
+ path.getParent().getFileName().toString(),
+ path.getFileName().toString()
+ );
+ return p.toString();
+ }
+
+ @Override
+ public Requirements getPlanedRequirements() {
+ return new Requirements( 0, 0 );
+ }
+
+ @Override
+ public boolean equals( Object obj ) {
+ if ( obj instanceof CopyTask ) {
+ return this.task.equals( ((CopyTask) obj).task );
+ }
+ else return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return task.hashCode() + 5;
+ }
+
+ public void finished(){
+ labelCounts.forEach( x -> x.taskIsNowOnNode( task, node ));
+ }
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/cluster/DataMissing.java b/src/main/java/cws/k8s/scheduler/model/cluster/DataMissing.java
new file mode 100644
index 00000000..50d597dc
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/cluster/DataMissing.java
@@ -0,0 +1,23 @@
+package cws.k8s.scheduler.model.cluster;
+
+import cws.k8s.scheduler.model.NodeWithAlloc;
+import cws.k8s.scheduler.model.Task;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.List;
+
+/**
+ * This class is used to create a triple of a task, a node and a label count.
+ * It is used to keep track which tasks are not on a node.
+ */
+@Getter
+@RequiredArgsConstructor
+public class DataMissing {
+
+ private final Task task;
+ private final NodeWithAlloc node;
+ private final List labelCounts;
+ private final double score;
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/cluster/DataMissingIntern.java b/src/main/java/cws/k8s/scheduler/model/cluster/DataMissingIntern.java
new file mode 100644
index 00000000..0bf9def4
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/cluster/DataMissingIntern.java
@@ -0,0 +1,19 @@
+package cws.k8s.scheduler.model.cluster;
+
+import cws.k8s.scheduler.model.NodeWithAlloc;
+import cws.k8s.scheduler.model.Task;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+/**
+ * This is a temporary class to group a task, a node and a label count.
+ */
+@RequiredArgsConstructor
+@Getter
+public class DataMissingIntern {
+
+ private final Task task;
+ private final NodeWithAlloc node;
+ private final LabelCount labelCount;
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/cluster/FakeTaskInputs.java b/src/main/java/cws/k8s/scheduler/model/cluster/FakeTaskInputs.java
new file mode 100644
index 00000000..2319f047
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/cluster/FakeTaskInputs.java
@@ -0,0 +1,32 @@
+package cws.k8s.scheduler.model.cluster;
+
+import cws.k8s.scheduler.model.location.Location;
+import cws.k8s.scheduler.model.taskinputs.PathFileLocationTriple;
+import cws.k8s.scheduler.model.taskinputs.TaskInputs;
+
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Fake task inputs are used to create {@link CopyTask}s that are not real tasks but are used to copy data between nodes.
+ * This task can only be run on the node that is included in the constructor.
+ */
+public class FakeTaskInputs extends TaskInputs {
+
+ private final Location includedNode;
+
+ public FakeTaskInputs( List files, Location includedNode ) {
+ super( new LinkedList<>(), files, null );
+ this.includedNode = includedNode;
+ }
+
+ @Override
+ public boolean canRunOnLoc( Location loc ) {
+ return includedNode == loc;
+ }
+
+ @Override
+ public boolean hasExcludedNodes() {
+ return true;
+ }
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/cluster/FileSizeRankScoreGroup.java b/src/main/java/cws/k8s/scheduler/model/cluster/FileSizeRankScoreGroup.java
new file mode 100644
index 00000000..68042667
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/cluster/FileSizeRankScoreGroup.java
@@ -0,0 +1,23 @@
+package cws.k8s.scheduler.model.cluster;
+
+import cws.k8s.scheduler.model.NodeWithAlloc;
+import cws.k8s.scheduler.model.Task;
+import cws.k8s.scheduler.util.score.FileSizeRankScore;
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public class FileSizeRankScoreGroup extends FileSizeRankScore {
+
+ private final GroupCluster groupCluster;
+
+ @Override
+ public long getScore( Task task, NodeWithAlloc location, long size ) {
+ final long score = super.getScore( task, location, size );
+ final long newScore = (long) (score * groupCluster.getScoreForTaskOnNode( task, location ));
+ if ( newScore < 1 ) {
+ return 1;
+ }
+ return newScore;
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/cluster/GroupCluster.java b/src/main/java/cws/k8s/scheduler/model/cluster/GroupCluster.java
new file mode 100644
index 00000000..f105ebb1
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/cluster/GroupCluster.java
@@ -0,0 +1,310 @@
+package cws.k8s.scheduler.model.cluster;
+
+import cws.k8s.scheduler.client.CWSKubernetesClient;
+import cws.k8s.scheduler.model.NodeWithAlloc;
+import cws.k8s.scheduler.model.Task;
+import cws.k8s.scheduler.model.location.hierachy.HierarchyWrapper;
+import cws.k8s.scheduler.model.location.hierachy.NoAlignmentFoundException;
+import cws.k8s.scheduler.model.location.hierachy.RealHierarchyFile;
+import cws.k8s.scheduler.model.outfiles.PathLocationWrapperPair;
+import cws.k8s.scheduler.model.taskinputs.PathFileLocationTriple;
+import cws.k8s.scheduler.model.taskinputs.TaskInputs;
+import cws.k8s.scheduler.scheduler.la2.TaskStat;
+import cws.k8s.scheduler.util.TaskNodeStats;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.file.Path;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@RequiredArgsConstructor
+public abstract class GroupCluster {
+
+ private final double minScoreToCopy = 0.5;
+ /**
+ * Group all tasks by label and deliver statistics
+ */
+ protected final Map countPerLabel = new HashMap<>();
+ /**
+ * tasks that are not yet scheduled
+ */
+ protected final LinkedList unscheduledTasks = new LinkedList<>();
+ /**
+ * tasks that are assigned to a task and running
+ */
+ protected final LinkedList scheduledTasks = new LinkedList<>();
+ /**
+ * tasks that have finished
+ */
+ protected final LinkedList finishedTasks = new LinkedList<>();
+ /**
+ * Map from label to node, which node is responsible for the label
+ */
+ protected final Map labelToNode = new HashMap<>();
+ /**
+ * Map from node to label, which labels are assigned to the node
+ */
+ protected final Map> nodeToLabel = new HashMap<>();
+ @Getter
+ private final HierarchyWrapper hierarchyWrapper;
+ @Getter
+ private final CWSKubernetesClient client;
+
+ /**
+ * When new tasks become available and are ready to schedule, this method is called to register them.
+ * @param tasks the tasks that are ready to schedule
+ */
+ public void tasksBecameAvailable( List tasks ) {
+ boolean anyWithLabel = false;
+ synchronized ( this ) {
+ unscheduledTasks.addAll( tasks );
+ for ( Task task : tasks ) {
+ if ( task.getOutLabel() == null || task.getOutLabel().isEmpty() ) {
+ continue;
+ }
+ for ( String label : task.getOutLabel() ) {
+ anyWithLabel = true;
+ final LabelCount labelCount = countPerLabel.computeIfAbsent( label, LabelCount::new );
+ labelCount.addWaitingTask( task );
+ }
+ }
+ // If we add at least one task with an outLabel we need to recalculate our alignment
+ if ( anyWithLabel ) {
+ recalculate();
+ }
+ }
+ }
+
+ /**
+ * This method is called when a task was scheduled to a node and can no longer be scheduled to another node.
+ * @param task the task that was scheduled
+ */
+ public void taskWasAssignedToNode( Task task ) {
+ synchronized ( this ) {
+ unscheduledTasks.remove( task );
+ scheduledTasks.add( task );
+ if ( task.getOutLabel() != null ) {
+ for ( String label : task.getOutLabel() ) {
+ countPerLabel.get( label ).makeTaskRunning( task );
+ }
+ recalculate();
+ }
+ }
+ }
+
+ /**
+ * This method is called when a task has finished and output data can be considered.
+ * @param tasks the tasks that have finished
+ */
+ public void tasksHaveFinished( List tasks ) {
+ synchronized ( this ) {
+ scheduledTasks.removeAll( tasks );
+ finishedTasks.addAll( tasks );
+ for ( Task task : tasks ) {
+ if ( task.getOutLabel() == null ) {
+ continue;
+ }
+ for ( String label : task.getOutLabel() ) {
+ countPerLabel.get( label ).makeTaskFinished( task );
+ }
+ }
+ }
+ }
+
+ /**
+ * This method is called when the state changes.
+ * This is either the case when a new task becomes available, or a task is scheduled.
+ */
+ abstract void recalculate();
+
+ /**
+ * Return the score for a task on a node.
+ * This calculates the score using jaccard similarity coefficient.
+ * Therefore, it calculates the shared labels between the task and the node and divides it by the total amount of labels of the task.
+ * @param task the task
+ * @param node the node
+ * @return the score in the range of 0 to 1, where 1 is the best score
+ */
+ public double getScoreForTaskOnNode( Task task, NodeWithAlloc node ) {
+ // If the task has no outLabel, it can run on any node
+ final Set outLabel = task.getOutLabel();
+ if ( outLabel == null || outLabel.isEmpty() ) {
+ return 1;
+ }
+
+ final Set nodeLabels = nodeToLabel.get( node );
+ return calculateJaccardSimilarityCoefficient( outLabel, nodeLabels );
+ }
+
+ /**
+ * Calculate the Jaccard similarity coefficient between two sets of labels.
+ * If there is no overlap, the coefficient is 0.01.
+ * @param outLabel the first set of labels, this cannot be empty or null
+ * @param nodeLabels the second set of labels, this can be null or empty
+ * @return the Jaccard similarity coefficient in the range of 0.01 to 1
+ */
+ static double calculateJaccardSimilarityCoefficient( Set outLabel, Set nodeLabels ) {
+ if ( outLabel == null || outLabel.isEmpty() ) {
+ throw new IllegalArgumentException( "outLabel cannot be empty or null" );
+ }
+ final double lowerBound = 0.01;
+ if ( nodeLabels == null || nodeLabels.isEmpty() ) {
+ return lowerBound;
+ }
+ outLabel = new HashSet<>( outLabel );
+ int outLabelSize = outLabel.size();
+ //intersection of both sets.
+ outLabel.retainAll( nodeLabels );
+ // No common labels
+ if ( outLabelSize == 0 ) {
+ return lowerBound;
+ }
+ final double result = (double) outLabel.size() / outLabelSize;
+ return Math.max( lowerBound, Math.min( 1, result )); // Clamp the result between 0.01 and 1
+ }
+
+ /**
+ * Assign a node to a label, such that tasks for this label can be prepared on the node.
+ * @param nodeLocation the node
+ * @param label the label
+ */
+ protected void addNodeToLabel( NodeWithAlloc nodeLocation, String label ) {
+ final NodeWithAlloc currentLocation = labelToNode.get( label );
+
+ if ( nodeLocation.equals( currentLocation ) ) {
+ return;
+ }
+
+ Set labels = nodeToLabel.computeIfAbsent( nodeLocation, k -> new HashSet<>() );
+ labels.add( label );
+
+ if ( currentLocation != null ) {
+ labels = nodeToLabel.get( currentLocation );
+ labels.remove( label );
+ }
+ labelToNode.put( label, nodeLocation );
+ }
+
+ /**
+ * Create TaskStat for tasks that could be prepared on a node, return maximal {@code maxCopiesPerNode} TaskStat per node.
+ * Only tasks with a score higher than {@link #minScoreToCopy} are considered.
+ * @param maxCopiesPerNode the maximum amount of copies that will be created for a node
+ * @return a list of all TaskStats that should be copied
+ */
+ public List getTaskStatToCopy( final int maxCopiesPerNode ){
+ synchronized ( this ) {
+ final Map tasksForLocation = new HashMap<>();
+ //all tasks where score > minScoreToCopy and tasks have at least one outLabel
+ final List tasksThatNeedToBeCopied = getTasksThatNeedToBeCopied();
+ List taskStats = new ArrayList<>();
+ for ( DataMissing dataMissing : tasksThatNeedToBeCopied ) {
+ Integer currentCopies = tasksForLocation.getOrDefault( dataMissing.getNode(), 0 );
+ // Only create maxCopiesPerNode possible TaskStats per node
+ TaskStat taskStat = currentCopies < maxCopiesPerNode ? getTaskStat( dataMissing ) : null;
+ // TaskStat is null if the output data was requested for a real task already and should not be copied anymore
+ if ( taskStat != null ) {
+ taskStats.add( taskStat );
+ tasksForLocation.put( dataMissing.getNode(), ++currentCopies );
+ }
+ }
+ return taskStats;
+ }
+ }
+
+ /**
+ * Create a TaskStat for a {@link DataMissing} object.
+ * @param dataMissing the DataMissing object
+ * @return the TaskStat or null if the output data was requested for a real task already
+ */
+ private TaskStat getTaskStat( DataMissing dataMissing ) {
+ final NodeWithAlloc node = dataMissing.getNode();
+ final OutputFiles outputFilesWrapper = dataMissing.getTask().getOutputFiles();
+ final Set outputFiles = outputFilesWrapper.getFiles();
+ // Do not check for OutputFiles#wasRequestedForRealTask() here,
+ // because it is checked when the DataMissing object is created in LabelCount.tasksNotOnNode
+ List files = outputFiles
+ .parallelStream()
+ .map( x -> convertToPathFileLocationTriple(x, dataMissing.getTask()) )
+ .collect( Collectors.toList() );
+ // If in the previous step at least one file was requested for a real task,
+ // all the output data is considered as requested for a real task
+ if ( dataMissing.getTask().getOutputFiles().isWasRequestedForRealTask() ) {
+ return null;
+ }
+ TaskInputs inputsOfTask = new FakeTaskInputs( files, node.getNodeLocation() );
+ final CopyTask copyTask = new CopyTask( dataMissing.getTask(), node, dataMissing.getLabelCounts() );
+ final TaskStat taskStat = new TaskStat( copyTask, inputsOfTask );
+ // TaskNodeStats is created with the total size and 0 for the data that is on the node
+ final TaskNodeStats taskNodeStats = new TaskNodeStats( inputsOfTask.calculateAvgSize(), 0, 0 );
+ taskStat.add( dataMissing.getNode(), taskNodeStats );
+ return taskStat;
+ }
+
+ /**
+ * Convert a PathLocationWrapperPair to a PathFileLocationTriple.
+ * Check for every file if it was requested for a real task.
+ * @param pair the PathLocationWrapperPair containing the LocationWrapper to process
+ * @param task the task that created the output data
+ * @return the PathFileLocationTriple or null if the output data was requested for a real task
+ */
+ private PathFileLocationTriple convertToPathFileLocationTriple( PathLocationWrapperPair pair, Task task ){
+ final Path path = pair.getPath();
+ RealHierarchyFile file = (RealHierarchyFile) hierarchyWrapper.getFile( path );
+ // if at least one file was requested for a real task, all the output data is considered as requested for a real task
+ // return null as we ignore this output data for proactively copying
+ if ( file.wasRequestedByTask() ) {
+ task.getOutputFiles().wasRequestedForRealTask();
+ return null;
+ }
+ try {
+ // task is the task that created the output data
+ final RealHierarchyFile.MatchingLocationsPair filesForTask = file.getFilesForTask( task );
+ return new PathFileLocationTriple( path, file, filesForTask.getMatchingLocations() );
+ } catch ( NoAlignmentFoundException e ) {
+ throw new RuntimeException( e );
+ }
+ }
+
+ /**
+ * Get all MissingData for tasks.
+ * This method checks for each task if the output data is on the node that is responsible for the label.
+ * If the data is not on the node, a DataMissing object is created. But only if the score is higher than {@link #minScoreToCopy}.
+ * @return a stream of DataMissing objects
+ */
+ private List getTasksThatNeedToBeCopied(){
+ final Map, List> collect = countPerLabel.entrySet()
+ .parallelStream()
+ .unordered()
+ .flatMap( v -> {
+ final String label = v.getKey();
+ final LabelCount value = v.getValue();
+ // which node is responsible for the label
+ final NodeWithAlloc node = labelToNode.get( label );
+ // get missing data for all tasks that are not on the node
+ return value.tasksNotOnNode( node );
+ } )
+ // Group all Labels for DataMissingIntern with the same task and node into a map
+ .collect( Collectors.groupingBy(
+ dmi -> Map.entry( dmi.getTask(), dmi.getNode() ),
+ Collectors.mapping( DataMissingIntern::getLabelCount, Collectors.toList() )
+ ) );
+ List result = new ArrayList<>( collect.size() );
+ for ( Map.Entry, List> e : collect.entrySet() ) {
+ final Task task = e.getKey().getKey();
+ final NodeWithAlloc node = e.getKey().getValue();
+ final List labelCounts = e.getValue();
+ final double score = getScoreForTaskOnNode( task, node );
+ // Only return tasks that have a score higher than minScoreToCopy
+ if ( score > minScoreToCopy ) {
+ result.add( new DataMissing( task, node, labelCounts, score ) );
+ }
+ }
+ // Sort the tasks by score in descending order: highest score first
+ result.sort( (x, y) -> Double.compare( y.getScore(), x.getScore() ) );
+ return result;
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/cluster/LabelCount.java b/src/main/java/cws/k8s/scheduler/model/cluster/LabelCount.java
new file mode 100644
index 00000000..96bc6b6a
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/cluster/LabelCount.java
@@ -0,0 +1,161 @@
+package cws.k8s.scheduler.model.cluster;
+
+import cws.k8s.scheduler.model.NodeWithAlloc;
+import cws.k8s.scheduler.model.Task;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.*;
+import java.util.stream.Stream;
+
+/**
+ * This class is used to keep track of the number of tasks with a specific label
+ * and on which nodes the tasks are running, waiting or there output is stored.
+ * This class is not thread safe!
+ */
+@RequiredArgsConstructor( access = AccessLevel.PACKAGE )
+public class LabelCount {
+
+ /**
+ * The label that all tasks have
+ */
+ @Getter
+ private final String label;
+
+ /**
+ * The tasks that are not yet started
+ */
+ @Getter
+ private final Set waitingTasks = new HashSet<>();
+
+ /**
+ * The tasks that are running
+ */
+ private final Set runningTasks = new HashSet<>();
+
+ /**
+ * The tasks that are finished
+ */
+ private final Set finishedTasks = new HashSet<>();
+
+ /**
+ * A Queue with a wrapper that contains the number of tasks running or finished on a node.
+ * The queue is sorted by the number of tasks running or finished on a node, such that the node with the most tasks is first.
+ */
+ @Getter
+ private final Queue runningOrfinishedOnNodes = new PriorityQueue<>();
+
+ /**
+ * For each node store the number of tasks that are running or ran on it,
+ * this map is the index for the elements in {@link #runningOrfinishedOnNodes}
+ */
+ private final Map nodeToShare = new HashMap<>();
+
+ /**
+ * For each task with this label store the nodes where the output data of the task is stored
+ */
+ private final Map> taskHasDataOnNode = new HashMap<>();
+
+ /**
+ * Get output data for all tasks that are not on the node
+ * @param node the node to check
+ * @return a stream of DataMissing objects containing the tasks' output data that is missing on the node
+ */
+ public Stream tasksNotOnNode( NodeWithAlloc node ) {
+ return taskHasDataOnNode.entrySet()
+ .stream()
+ // check if the task's output is not on the node and the output data was not requested for a real task
+ .filter( taskSetEntry -> !taskSetEntry.getValue().contains( node )
+ && !taskSetEntry.getKey().getOutputFiles().isWasRequestedForRealTask() )
+ .map( x -> new DataMissingIntern( x.getKey(), node, this ) );
+ }
+
+ /**
+ * Get the number of tasks with this label that are not yet started
+ * @return the number of tasks with this label that are not yet started
+ */
+ public int getCountWaiting() {
+ return waitingTasks.size();
+ }
+
+ /**
+ * Get the number of tasks with this label that are running
+ * @return the number of tasks with this label that are running
+ */
+ public int getCountRunning() {
+ return runningTasks.size();
+ }
+
+ /**
+ * Get the number of tasks with this label that are finished
+ * @return the number of tasks with this label that are finished
+ */
+ public int getCountFinished() {
+ return finishedTasks.size();
+ }
+
+ /**
+ * Get the number of tasks with this label
+ * @return the number of tasks with this label
+ */
+ public int getCount(){
+ return getCountWaiting() + getCountRunning() + getCountFinished();
+ }
+
+ /**
+ * Add a task to the label count, the task is not yet started
+ * the task needs to have this label, however the label is not checked
+ * @param task the task to add with this label
+ */
+ public void addWaitingTask( Task task ){
+ waitingTasks.add(task);
+ }
+
+ /**
+ * Make a task running, the task needs to have this label, however the label is not checked
+ * Adds the task to {@link #runningOrfinishedOnNodes}
+ * @param task the task to make running
+ */
+ public void makeTaskRunning( Task task ) {
+ final NodeWithAlloc node = task.getNode();
+ final TasksOnNodeWrapper tasksOnNodeWrapper = nodeToShare.computeIfAbsent(node, k -> {
+ TasksOnNodeWrapper newWrapper = new TasksOnNodeWrapper(k);
+ runningOrfinishedOnNodes.add(newWrapper);
+ return newWrapper;
+ });
+ tasksOnNodeWrapper.addRunningTask();
+ final boolean remove = waitingTasks.remove( task );
+ if ( !remove ) {
+ throw new IllegalStateException( "Task " + task + " was not in waiting tasks" );
+ }
+ runningTasks.add( task );
+ }
+
+ /**
+ * Make a task finished, the task needs to have this label, however the label is not checked
+ * adds the task to {@link #taskHasDataOnNode}, to mark that the output data is on the node
+ * @param task the task to make finished
+ */
+ public void makeTaskFinished( Task task ) {
+ final boolean remove = runningTasks.remove( task );
+ if ( !remove ) {
+ throw new IllegalStateException( "Task " + task + " was not in running tasks" );
+ }
+ finishedTasks.add( task );
+ final HashSet locations = new HashSet<>();
+ locations.add( task.getNode() );
+ taskHasDataOnNode.put( task, locations );
+ }
+
+ /**
+ * Mark that the task's output data is on a node.
+ * This method is called after the task was copied to the node.
+ * @param task the task
+ * @param node the node the task's output was copied to
+ */
+ public void taskIsNowOnNode( Task task, NodeWithAlloc node ) {
+ taskHasDataOnNode.get( task ).add( node );
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/cws/k8s/scheduler/model/cluster/MostOutLabelsComparator.java b/src/main/java/cws/k8s/scheduler/model/cluster/MostOutLabelsComparator.java
new file mode 100644
index 00000000..42dd1d98
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/cluster/MostOutLabelsComparator.java
@@ -0,0 +1,34 @@
+package cws.k8s.scheduler.model.cluster;
+
+import cws.k8s.scheduler.scheduler.la2.TaskStat;
+import cws.k8s.scheduler.util.TaskNodeStats;
+import lombok.RequiredArgsConstructor;
+
+import java.util.Comparator;
+
+@RequiredArgsConstructor
+public class MostOutLabelsComparator implements Comparator {
+
+ private final GroupCluster groupCluster;
+
+ @Override
+ public int compare( TaskStat.NodeAndStatWrapper o1, TaskStat.NodeAndStatWrapper o2 ) {
+ final TaskNodeStats o1Stats = o1.getTaskNodeStats();
+ final TaskNodeStats o2Stats = o2.getTaskNodeStats();
+ //same task does not necessarily have the same size
+ if ( o1.getTask() == o2.getTask() || o1Stats.getTaskSize() == o2Stats.getTaskSize() ) {
+ double ratingO1 = groupCluster.getScoreForTaskOnNode( o1.getTask(), o1.getNode() );
+ double ratingO2 = groupCluster.getScoreForTaskOnNode( o2.getTask(), o2.getNode() );
+ // Use the node with a higher rating
+ if ( ratingO1 != ratingO2 ) {
+ return Double.compare( ratingO2, ratingO1 );
+ }
+ //Prefer the one with fewer remaining data
+ return Long.compare( o1Stats.getSizeRemaining(), o2Stats.getSizeRemaining() );
+ } else {
+ //Prefer the one with larger task size
+ return Long.compare( o2Stats.getTaskSize(), o1Stats.getTaskSize() );
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/cws/k8s/scheduler/model/cluster/OutputFiles.java b/src/main/java/cws/k8s/scheduler/model/cluster/OutputFiles.java
new file mode 100644
index 00000000..4f3e67c3
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/cluster/OutputFiles.java
@@ -0,0 +1,28 @@
+package cws.k8s.scheduler.model.cluster;
+
+import cws.k8s.scheduler.model.outfiles.PathLocationWrapperPair;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.Set;
+
+@Slf4j
+@Getter
+@RequiredArgsConstructor
+public class OutputFiles {
+
+
+ private final Set files;
+
+ /**
+ * this is used to check if the output data was requested for a real task
+ * As soon as one file is requested for a real task, the output data is considered as requested for a real task
+ */
+ private boolean wasRequestedForRealTask = false;
+
+ public void wasRequestedForRealTask(){
+ wasRequestedForRealTask = true;
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/cluster/RankAndMinCopyingComparatorCopyTasks.java b/src/main/java/cws/k8s/scheduler/model/cluster/RankAndMinCopyingComparatorCopyTasks.java
new file mode 100644
index 00000000..e057345a
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/cluster/RankAndMinCopyingComparatorCopyTasks.java
@@ -0,0 +1,27 @@
+package cws.k8s.scheduler.model.cluster;
+
+import cws.k8s.scheduler.scheduler.la2.RankAndMinCopyingComparator;
+import cws.k8s.scheduler.scheduler.la2.TaskStat;
+
+import java.util.Comparator;
+
+public class RankAndMinCopyingComparatorCopyTasks extends RankAndMinCopyingComparator {
+
+ public RankAndMinCopyingComparatorCopyTasks( Comparator comparator ) {
+ super( comparator );
+ }
+
+ @Override
+ public int compare( TaskStat o1, TaskStat o2 ) {
+ if ( o1.getTask() instanceof CopyTask ^ o2.getTask() instanceof CopyTask ) {
+ if ( o1.getTask() instanceof CopyTask ) {
+ return 1;
+ } else {
+ return -1;
+ }
+ } else {
+ return super.compare( o1, o2 );
+ }
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/cluster/SimpleGroupCluster.java b/src/main/java/cws/k8s/scheduler/model/cluster/SimpleGroupCluster.java
new file mode 100644
index 00000000..2297019b
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/cluster/SimpleGroupCluster.java
@@ -0,0 +1,116 @@
+package cws.k8s.scheduler.model.cluster;
+
+import cws.k8s.scheduler.client.CWSKubernetesClient;
+import cws.k8s.scheduler.model.NodeWithAlloc;
+import cws.k8s.scheduler.model.location.hierachy.HierarchyWrapper;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+public class SimpleGroupCluster extends GroupCluster {
+
+ public SimpleGroupCluster( HierarchyWrapper hierarchyWrapper, CWSKubernetesClient client ) {
+ super( hierarchyWrapper, client );
+ }
+
+ @Override
+ void recalculate() {
+ final List allNodes = getClient().getAllNodes();
+
+ // Filter labels that have waiting tasks.
+ final List> labelsWithWaitingTasks = countPerLabel
+ .entrySet()
+ .stream()
+ // If nothing waiting, we do not need a suitable node, so we do not reassign it
+ // If there is only one task with a label it can run where ever resources are available. Do not force a location.
+ .filter( kv -> kv.getValue().getCountWaiting() > 0 && kv.getValue().getCount() > 1 )
+ // Sort reverse by count
+ .sorted( Comparator.comparingInt( kv -> - kv.getValue().getCount() ) )
+ .collect( Collectors.toList() );
+
+ // store how many tasks would have been executed on every node with the current alignment
+ // this is an approximation since tasks can have multiple labels and would appear multiple times
+ Map tasksOnNode = new HashMap<>();
+
+ // For every label and node count how many tasks could run (affinities match), we ignore the current load completely
+ for ( Map.Entry labelWithWaitingTasks : labelsWithWaitingTasks ) {
+ Queue xTasksCanRunOnNode = new PriorityQueue<>();
+ for ( NodeWithAlloc node : allNodes ) {
+ final long count = labelWithWaitingTasks
+ .getValue()
+ .getWaitingTasks()
+ .stream()
+ .filter( task -> node.affinitiesMatch( task.getPod() ) )
+ .count();
+ if ( count > 0 ) {
+ final TasksOnNodeWrapper tasksOnNodeWrapper = new TasksOnNodeWrapper( node, (int) count );
+ xTasksCanRunOnNode.add( tasksOnNodeWrapper );
+ }
+ }
+ if ( !xTasksCanRunOnNode.isEmpty() ) {
+ final NodeWithAlloc node = calculateBestFittingNode( labelWithWaitingTasks.getKey(), xTasksCanRunOnNode, labelWithWaitingTasks.getValue(), tasksOnNode );
+ if ( node != null ) {
+ addNodeToLabel( node, labelWithWaitingTasks.getKey() );
+ tasksOnNode.put( node, tasksOnNode.getOrDefault( node, 0 ) + labelWithWaitingTasks.getValue().getCountWaiting() );
+ }
+ }
+ }
+ }
+
+ /**
+ * This method first looks which nodes ran the most tasks with the current label already and then selects the node where the most tasks can run.
+ * @param label
+ * @param xTasksCanRunOnNode
+ * @param labelCount
+ * @param tasksOnNode
+ * @return
+ */
+ private NodeWithAlloc calculateBestFittingNode( String label, Queue xTasksCanRunOnNode, LabelCount labelCount, Map tasksOnNode ) {
+ if ( xTasksCanRunOnNode.isEmpty() ) {
+ return null;
+ }
+ final Set bestFittingNodes = new HashSet<>();
+ final TasksOnNodeWrapper bestFittingNode = xTasksCanRunOnNode.poll();
+ bestFittingNodes.add( bestFittingNode.getNode() );
+ while( !xTasksCanRunOnNode.isEmpty() && xTasksCanRunOnNode.peek().getShare() == bestFittingNode.getShare() ){
+ bestFittingNodes.add( xTasksCanRunOnNode.poll().getNode() );
+ }
+
+ final List runningOrfinishedOnNodes = new ArrayList<>(labelCount.getRunningOrfinishedOnNodes());
+ if ( runningOrfinishedOnNodes.isEmpty() ) {
+ // if tasks with this label have not been executed
+ return findBestFittingNode( bestFittingNodes, tasksOnNode );
+ }
+
+ for ( TasksOnNodeWrapper tasksOnNodeWrapper : runningOrfinishedOnNodes ) {
+ if ( bestFittingNodes.contains( tasksOnNodeWrapper.getNode() ) ) {
+ return tasksOnNodeWrapper.getNode();
+ }
+ }
+
+ final NodeWithAlloc nodeLocation = calculateBestFittingNode( label, xTasksCanRunOnNode, labelCount, tasksOnNode );
+ if ( nodeLocation != null ) {
+ return nodeLocation;
+ }
+ // If no node is found, return a random one
+ return findBestFittingNode( bestFittingNodes, tasksOnNode );
+
+ }
+
+ /**
+ * Assign to the node that has the least tasks to process yet
+ * @param bestFittingNodes list of potential nodes
+ * @param tasksOnNode map of tasks on each node
+ * @return
+ */
+ private NodeWithAlloc findBestFittingNode( final Set bestFittingNodes, Map tasksOnNode ) {
+ NodeWithAlloc best = null;
+ for ( NodeWithAlloc fittingNode : bestFittingNodes ) {
+ if ( best == null || tasksOnNode.getOrDefault( fittingNode, 0 ) < tasksOnNode.getOrDefault( best, 0 ) ) {
+ best = fittingNode;
+ }
+ }
+ return best;
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/cluster/TasksOnNodeWrapper.java b/src/main/java/cws/k8s/scheduler/model/cluster/TasksOnNodeWrapper.java
new file mode 100644
index 00000000..0d8066c6
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/cluster/TasksOnNodeWrapper.java
@@ -0,0 +1,30 @@
+package cws.k8s.scheduler.model.cluster;
+
+import cws.k8s.scheduler.model.NodeWithAlloc;
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import org.jetbrains.annotations.NotNull;
+
+/**
+ * This class is used to keep track of the number of tasks with a specific label on a node.
+ */
+@Getter
+@EqualsAndHashCode
+@RequiredArgsConstructor
+@AllArgsConstructor
+public class TasksOnNodeWrapper implements Comparable {
+
+ private final NodeWithAlloc node;
+ private int share = 0;
+
+ @Override
+ public int compareTo( @NotNull TasksOnNodeWrapper o ) {
+ return Integer.compare( o.share, share );
+ }
+
+ public void addRunningTask() {
+ share++;
+ }
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/location/Location.java b/src/main/java/cws/k8s/scheduler/model/location/Location.java
new file mode 100644
index 00000000..ad4cc207
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/location/Location.java
@@ -0,0 +1,30 @@
+package cws.k8s.scheduler.model.location;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+public abstract class Location implements Serializable {
+
+ public abstract String getIdentifier();
+
+ public abstract LocationType getType();
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof Location)) {
+ return false;
+ }
+ Location that = (Location) o;
+ return ( getType() == that.getType() ) && ( getIdentifier().equals( that.getIdentifier() ));
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getIdentifier(),getType());
+ }
+
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/location/LocationType.java b/src/main/java/cws/k8s/scheduler/model/location/LocationType.java
new file mode 100644
index 00000000..ed623a2f
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/location/LocationType.java
@@ -0,0 +1,7 @@
+package cws.k8s.scheduler.model.location;
+
+public enum LocationType {
+
+ NODE
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/location/NodeLocation.java b/src/main/java/cws/k8s/scheduler/model/location/NodeLocation.java
new file mode 100644
index 00000000..c97b585e
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/location/NodeLocation.java
@@ -0,0 +1,70 @@
+package cws.k8s.scheduler.model.location;
+
+import io.fabric8.kubernetes.api.model.Node;
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+@RequiredArgsConstructor( access = AccessLevel.PRIVATE )
+public class NodeLocation extends Location {
+
+ private static final long serialVersionUID = 1L;
+
+ private static final ConcurrentMap< String, NodeLocation > locationHolder = new ConcurrentHashMap<>();
+
+ @Getter
+ private final String identifier;
+
+ public static NodeLocation getLocation( Node node ){
+ return getLocation( node.getMetadata().getName() );
+ }
+
+ public static NodeLocation getLocation( String node ){
+ if ( node == null ) {
+ throw new IllegalArgumentException("Node cannot be null");
+ }
+ final NodeLocation nodeLocation = locationHolder.get(node);
+ if ( nodeLocation == null ){
+ locationHolder.putIfAbsent( node, new NodeLocation( node ) );
+ return locationHolder.get( node );
+ }
+ return nodeLocation;
+ }
+
+ @Override
+ public LocationType getType() {
+ return LocationType.NODE;
+ }
+
+ @Override
+ public String toString() {
+ return "Node(" + identifier + ")";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof NodeLocation)) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+
+ NodeLocation that = (NodeLocation) o;
+
+ return getIdentifier() != null ? getIdentifier().equals(that.getIdentifier()) : that.getIdentifier() == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = super.hashCode();
+ result = 31 * result + (getIdentifier() != null ? getIdentifier().hashCode() : 0);
+ return result;
+ }
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/location/hierachy/AbstractHierarchyFile.java b/src/main/java/cws/k8s/scheduler/model/location/hierachy/AbstractHierarchyFile.java
new file mode 100644
index 00000000..3a431269
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/location/hierachy/AbstractHierarchyFile.java
@@ -0,0 +1,5 @@
+package cws.k8s.scheduler.model.location.hierachy;
+
+public abstract class AbstractHierarchyFile extends HierarchyFile {
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/location/hierachy/Folder.java b/src/main/java/cws/k8s/scheduler/model/location/hierachy/Folder.java
new file mode 100644
index 00000000..ef4c0ae4
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/location/hierachy/Folder.java
@@ -0,0 +1,84 @@
+package cws.k8s.scheduler.model.location.hierachy;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.file.Path;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+@Slf4j
+@NoArgsConstructor(access = AccessLevel.PACKAGE)
+public class Folder extends HierarchyFile {
+
+ private final ConcurrentMap children = new ConcurrentHashMap<>();
+
+ @Override
+ public boolean isDirectory() {
+ return true;
+ }
+
+ @Override
+ public boolean isSymlink() {
+ return false;
+ }
+
+ public HierarchyFile get(String name ){
+ return children.get( name );
+ }
+
+ /**
+ * Creates folder if not existing
+ * @param name name of the folder to create
+ * @return the file with the name, or the new created folder
+ */
+ public Folder getOrCreateFolder(String name ){
+ final HierarchyFile file = children.get(name);
+ if( file == null || !file.isDirectory() ){
+ return (Folder) children.compute( name, (key,value) -> (value == null || !value.isDirectory()) ? new Folder() : value );
+ }
+ return (Folder) file;
+ }
+
+ public Map getAllChildren(Path currentPath ){
+ Map result = new TreeMap<>();
+ getAllChildren( result, currentPath );
+ return result;
+ }
+
+ private void getAllChildren(final Map result, Path currentPath ){
+ for (Map.Entry entry : children.entrySet()) {
+ Path resolve = currentPath.resolve(entry.getKey());
+ if ( !entry.getValue().isSymlink() && entry.getValue().isDirectory() ){
+ final Folder value = (Folder) entry.getValue();
+ value.getAllChildren( result, resolve );
+ } else {
+ result.put( resolve, (AbstractHierarchyFile) entry.getValue());
+ }
+ }
+ }
+
+ public LocationWrapper addOrUpdateFile(final String name, boolean overwrite, final LocationWrapper location ) {
+ final RealHierarchyFile file = (RealHierarchyFile) children.compute( name, (k, v) -> {
+ if (v == null || v.isDirectory() || v.isSymlink() ) {
+ return new RealHierarchyFile( location );
+ }
+ return v;
+ } );
+ return file.addOrUpdateLocation( overwrite, location );
+ }
+
+ public boolean addSymlink( final String name, final Path dst ){
+ children.compute( name, (k,v) -> {
+ if ( v == null || !v.isSymlink() || !((LinkHierarchyFile) v).getDst().equals(dst) ) {
+ return new LinkHierarchyFile( dst );
+ }
+ return v;
+ } );
+ return true;
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/location/hierachy/HierarchyFile.java b/src/main/java/cws/k8s/scheduler/model/location/hierachy/HierarchyFile.java
new file mode 100644
index 00000000..a32b6226
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/location/hierachy/HierarchyFile.java
@@ -0,0 +1,9 @@
+package cws.k8s.scheduler.model.location.hierachy;
+
+public abstract class HierarchyFile {
+
+ public abstract boolean isDirectory();
+
+ public abstract boolean isSymlink();
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/location/hierachy/HierarchyWrapper.java b/src/main/java/cws/k8s/scheduler/model/location/hierachy/HierarchyWrapper.java
new file mode 100644
index 00000000..0d2069a4
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/location/hierachy/HierarchyWrapper.java
@@ -0,0 +1,169 @@
+package cws.k8s.scheduler.model.location.hierachy;
+
+import lombok.extern.slf4j.Slf4j;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+@Slf4j
+public class HierarchyWrapper {
+
+ private final Path workdir;
+
+ private final ConcurrentMap workDirs = new ConcurrentHashMap<>(2);
+
+ public HierarchyWrapper( String workdir ) {
+ if ( workdir == null ) {
+ throw new IllegalArgumentException( "Workdir is not defined" );
+ }
+ this.workdir = Paths.get( workdir ).normalize();
+ }
+
+ private Path relativize( Path path ){
+ return workdir.relativize( path ).normalize();
+ }
+
+ private Folder getWorkdir( Iterator iterator, boolean create ){
+ if(!iterator.hasNext()) {
+ return null;
+ }
+ final String hash1 = iterator.next().toString();
+ if(!iterator.hasNext()) {
+ return null;
+ }
+ final String hash2 = iterator.next().toString();
+ final String key = hash1 + hash2;
+ final Folder folder = workDirs.get( key );
+ if( create && folder == null ){
+ workDirs.putIfAbsent( key, new Folder());
+ return workDirs.get( key );
+ }
+ return folder;
+ }
+
+ /**
+ *
+ * @param path get all files recursively in this folder (absolute path)
+ * @return Null if folder is empty, or not found
+ */
+ public Map getAllFilesInDir(final Path path ){
+ final Path relativePath = relativize( path );
+ Iterator iterator = relativePath.iterator();
+ HierarchyFile current = getWorkdir( iterator, false );
+ if( current == null ) {
+ return null;
+ }
+ while(iterator.hasNext()){
+ Path p = iterator.next();
+ if ( current != null && current.isDirectory() ){
+ current = ((Folder) current).get( p.toString() );
+ } else {
+ return null;
+ }
+ }
+ if( current.isDirectory() ) {
+ return ((Folder) current).getAllChildren( path.normalize() );
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ *
+ * @param path file to add (absolute path)
+ * @param location location where the file is located
+ * @return null if file can not be created
+ */
+ public LocationWrapper addFile(final Path path, final LocationWrapper location ){
+ return addFile( path, false, location );
+ }
+
+ public LocationWrapper addFile(final Path path, boolean overwrite, final LocationWrapper location ){
+
+ final Folder folderToInsert = findFolderToInsert(path);
+
+ if( folderToInsert == null ) {
+ return null;
+ } else {
+ return folderToInsert.addOrUpdateFile( path.getFileName().toString(), overwrite, location );
+ }
+
+ }
+
+ public boolean addSymlink( final Path src, final Path dst ){
+
+ final Folder folderToInsert = findFolderToInsert( src );
+
+ if( folderToInsert == null ) {
+ return false;
+ } else {
+ return folderToInsert.addSymlink( src.getFileName().toString(), dst );
+ }
+
+ }
+
+ private Folder findFolderToInsert( final Path path ){
+ final Path relativePath = relativize( path );
+ if (relativePath.startsWith("..")){
+ return null;
+ }
+ Iterator iterator = relativePath.iterator();
+ Folder current = getWorkdir( iterator, true );
+ if( current == null ) {
+ return null;
+ }
+ while(iterator.hasNext()) {
+ Path p = iterator.next();
+ if( iterator.hasNext() ){
+ //folder
+ current = current.getOrCreateFolder( p.toString() );
+ } else {
+ //file
+ return current;
+ }
+ }
+ //This would add a file in working hierarchy
+ return null;
+ }
+
+ /**
+ *
+ * @param path file to get (absolute path)
+ * @return File or null if file does not exist
+ */
+ public HierarchyFile getFile(Path path ){
+ final Path relativePath = relativize( path );
+ if (relativePath.startsWith("..")){
+ return null;
+ }
+ Iterator iterator = relativePath.iterator();
+ Folder current = getWorkdir( iterator, false );
+ if( current == null ) {
+ return null;
+ }
+ if( !iterator.hasNext() ) {
+ return current;
+ }
+ while( iterator.hasNext() ) {
+ Path p = iterator.next();
+ final HierarchyFile file = current.get( p.toString() );
+ if( iterator.hasNext() && file.isDirectory() ){
+ //folder
+ current = (Folder) file;
+ } else if ( !iterator.hasNext() ) {
+ return file;
+ } else {
+ break;
+ }
+ }
+ return null;
+ }
+
+ public boolean isInScope( Path path ){
+ return path.startsWith( workdir );
+ }
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/location/hierachy/LinkHierarchyFile.java b/src/main/java/cws/k8s/scheduler/model/location/hierachy/LinkHierarchyFile.java
new file mode 100644
index 00000000..7b36b85c
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/location/hierachy/LinkHierarchyFile.java
@@ -0,0 +1,23 @@
+package cws.k8s.scheduler.model.location.hierachy;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.nio.file.Path;
+
+@RequiredArgsConstructor
+public class LinkHierarchyFile extends AbstractHierarchyFile {
+
+ @Getter
+ private final Path dst;
+
+ @Override
+ public boolean isDirectory() {
+ throw new IllegalStateException("Call on link");
+ }
+
+ @Override
+ public boolean isSymlink() {
+ return true;
+ }
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/location/hierachy/LocationWrapper.java b/src/main/java/cws/k8s/scheduler/model/location/hierachy/LocationWrapper.java
new file mode 100644
index 00000000..ff3c3e5e
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/location/hierachy/LocationWrapper.java
@@ -0,0 +1,142 @@
+package cws.k8s.scheduler.model.location.hierachy;
+
+import cws.k8s.scheduler.model.Task;
+import cws.k8s.scheduler.model.location.Location;
+import lombok.Getter;
+
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicLong;
+
+@Getter
+public class LocationWrapper {
+
+ private static final AtomicLong nextID = new AtomicLong(0);
+
+ private final long id = nextID.getAndIncrement();
+ private final Location location;
+ private long timestamp;
+ private long sizeInBytes;
+ private long createTime = System.currentTimeMillis();
+ private Task createdByTask;
+ private LocationWrapper copyOf;
+ //Deactivated if file was maybe not copied completely or if one file was changed by the workflow engine.
+ private boolean active = true;
+ private int inUse = 0;
+
+ public LocationWrapper(Location location, long timestamp, long sizeInBytes) {
+ this( location, timestamp, sizeInBytes ,null);
+ }
+
+ public LocationWrapper(Location location, long timestamp, long sizeInBytes, Task task) {
+ this( location, timestamp, sizeInBytes ,task, null );
+ }
+
+ private LocationWrapper(Location location, long timestamp, long sizeInBytes, Task createdByTask, LocationWrapper copyOf) {
+ this.location = location;
+ this.timestamp = timestamp;
+ this.sizeInBytes = sizeInBytes;
+ this.createdByTask = createdByTask;
+ this.copyOf = copyOf;
+ }
+
+ public void update( LocationWrapper update ){
+ if (location != update.location) {
+ throw new IllegalArgumentException( "Can only update LocationWrapper with the same location." );
+ }
+ synchronized ( this ) {
+ this.timestamp = update.timestamp;
+ this.sizeInBytes = update.sizeInBytes;
+ this.createTime = update.createTime;
+ this.createdByTask = update.createdByTask;
+ this.copyOf = update.copyOf;
+ this.active = update.active;
+ }
+ }
+
+ public void deactivate(){
+ this.active = false;
+ }
+
+ /**
+ * use the file, if you copy it to a node, or a task uses it as input
+ */
+ public void use(){
+ synchronized ( this ) {
+ inUse++;
+ }
+ }
+
+ /**
+ * free the file, if you finished copy it to a node, or a task the task finished that used it as an input
+ */
+ public void free(){
+ synchronized ( this ) {
+ inUse--;
+ }
+ }
+
+ /**
+ * Any task currently reading or writing to this file
+ */
+ public boolean isInUse(){
+ return inUse > 0;
+ }
+
+ public LocationWrapper getCopyOf( Location location ) {
+ synchronized ( this ) {
+ return new LocationWrapper(location, timestamp, sizeInBytes, createdByTask, copyOf == null ? this : copyOf);
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof LocationWrapper)) {
+ return false;
+ }
+
+ LocationWrapper that = (LocationWrapper) o;
+
+ synchronized ( this ) {
+ if (getTimestamp() != that.getTimestamp()) {
+ return false;
+ }
+ if (getSizeInBytes() != that.getSizeInBytes()) {
+ return false;
+ }
+ if (!getLocation().equals(that.getLocation())) {
+ return false;
+ }
+ if (getCreatedByTask() != null ? !getCreatedByTask().equals(that.getCreatedByTask()) : that.getCreatedByTask() != null) {
+ return false;
+ }
+ return getCopyOf() != null ? getCopyOf().equals(that.getCopyOf()) : that.getCopyOf() == null;
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ synchronized ( this ) {
+ return Objects.hash(getLocation(), getTimestamp(), getSizeInBytes(), getCreatedByTask());
+ }
+ }
+
+ @Override
+ public String toString() {
+ synchronized ( this ) {
+ return "LocationWrapper{" +
+ "id=" + id +
+ ", active=" + active +
+ ", location=" + location.getIdentifier() +
+ ", timestamp=" + timestamp +
+ ", inUse=" + inUse + "x" +
+ ", sizeInBytes=" + sizeInBytes +
+ ", createTime=" + createTime +
+ ", createdByTask=" + createdByTask +
+ ", copyOf=" + copyOf +
+ '}';
+ }
+ }
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/location/hierachy/NoAlignmentFoundException.java b/src/main/java/cws/k8s/scheduler/model/location/hierachy/NoAlignmentFoundException.java
new file mode 100644
index 00000000..e73de13a
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/location/hierachy/NoAlignmentFoundException.java
@@ -0,0 +1,4 @@
+package cws.k8s.scheduler.model.location.hierachy;
+
+public class NoAlignmentFoundException extends Exception {
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/location/hierachy/RealHierarchyFile.java b/src/main/java/cws/k8s/scheduler/model/location/hierachy/RealHierarchyFile.java
new file mode 100644
index 00000000..27921a61
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/location/hierachy/RealHierarchyFile.java
@@ -0,0 +1,317 @@
+package cws.k8s.scheduler.model.location.hierachy;
+
+import cws.k8s.scheduler.dag.Process;
+import cws.k8s.scheduler.model.Task;
+import cws.k8s.scheduler.model.location.Location;
+import cws.k8s.scheduler.model.location.LocationType;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.*;
+
+@Slf4j
+public class RealHierarchyFile extends AbstractHierarchyFile {
+
+ /**
+ * This field contains the newest LocationWrapper of one file for each node.
+ */
+ @Getter
+ private LocationWrapper[] locations;
+ static final String LOCATION_IS_NULL = "location is null";
+ private boolean wasRequestedByTask = false;
+
+ public RealHierarchyFile( LocationWrapper location ) {
+ if ( location == null ) {
+ throw new IllegalArgumentException( LOCATION_IS_NULL );
+ }
+ this.locations = new LocationWrapper[]{ location };
+ }
+
+ @Override
+ public boolean isDirectory(){
+ return false;
+ }
+
+ @Override
+ public boolean isSymlink() {
+ return false;
+ }
+
+ public void removeLocation( LocationWrapper location ){
+ if ( location == null ) {
+ throw new IllegalArgumentException( LOCATION_IS_NULL );
+ }
+ synchronized ( this ){
+ for ( LocationWrapper locationWrapper : locations ) {
+ if ( location.getLocation().equals( locationWrapper.getLocation() ) ) {
+ locationWrapper.deactivate();
+ }
+ }
+ }
+ }
+
+ public LocationWrapper addOrUpdateLocation( boolean overwrite, LocationWrapper location ){
+ if ( location == null ) {
+ throw new IllegalArgumentException( LOCATION_IS_NULL );
+ }
+ synchronized ( this ){
+ LocationWrapper locationWrapperToUpdate = null;
+ for (LocationWrapper locationWrapper : locations) {
+ if ( location.getLocation().equals( locationWrapper.getLocation() ) ) {
+ locationWrapperToUpdate = locationWrapper;
+ if ( overwrite || location.getTimestamp() > locationWrapper.getTimestamp() ) {
+ locationWrapperToUpdate.update( location );
+ }
+ if ( !overwrite ) {
+ return locationWrapperToUpdate;
+ }
+ } else if ( overwrite ){
+ locationWrapper.deactivate();
+ }
+ }
+ if ( overwrite && locationWrapperToUpdate != null ) {
+ return locationWrapperToUpdate;
+ }
+ final LocationWrapper[] newLocation = Arrays.copyOf(locations, locations.length + 1);
+ newLocation[ locations.length ] = location;
+ locations = newLocation;
+ }
+ return location;
+ }
+
+ private List combineResultsWithInitial (
+ LinkedList current,
+ LinkedList ancestors,
+ LinkedList descendants,
+ LinkedList unrelated,
+ LinkedList initial
+ ) {
+ LinkedList result;
+ long time = 0;
+ //Only keep last update
+ if ( initial.size() > 1 ) {
+ result = new LinkedList<>();
+ for (LocationWrapper locationWrapper : initial) {
+ if ( locationWrapper.getCreateTime() > time ) {
+ result.clear();
+ result.add( locationWrapper );
+ time = locationWrapper.getCreateTime();
+ } else if ( locationWrapper.getCreateTime() == time ) {
+ result.add( locationWrapper );
+ }
+ }
+ } else {
+ time = initial.get(0).getCreateTime();
+ result = initial;
+ }
+ addAllLaterLocationsToResult( current, result, time );
+ if( current == null ) {
+ addAllLaterLocationsToResult( ancestors, result, time );
+ }
+ addAllLaterLocationsToResult( unrelated, result, time );
+ addAllLaterLocationsToResult( descendants, result, time );
+ return result;
+ }
+
+ private List combineResultsEmptyInitial (
+ LinkedList current,
+ LinkedList ancestors,
+ LinkedList descendants,
+ LinkedList unrelated
+ ) {
+ LinkedList result = null;
+ if ( current != null ) {
+ result = current;
+ }
+ if ( ancestors != null ) {
+ if (current == null ) {
+ result = ancestors;
+ } else {
+ result.addAll( ancestors );
+ }
+ }
+ if ( unrelated != null ) {
+ if ( result == null ) {
+ result = unrelated;
+ } else {
+ result.addAll( unrelated );
+ }
+ }
+ if ( descendants != null ) {
+ if ( result == null ) {
+ result = descendants;
+ } else {
+ result.addAll( descendants );
+ }
+ }
+ return result;
+ }
+
+ private void addToAncestors(LinkedList ancestors, LocationWrapper location, Process locationProcess) {
+ //Add location to list if it could be the last version
+ final Iterator iterator = ancestors.iterator();
+ Set locationAncestors = null;
+ while (iterator.hasNext()) {
+ final LocationWrapper next = iterator.next();
+ final Process currentProcess = next.getCreatedByTask().getProcess();
+ if (locationProcess == currentProcess) {
+ break;
+ } else {
+ if( locationAncestors == null ) {
+ locationAncestors = locationProcess.getAncestors();
+ }
+ if ( locationAncestors.contains(currentProcess) ) {
+ iterator.remove();
+ } else if (locationProcess.getDescendants().contains(currentProcess)) {
+ return;
+ }
+ }
+ }
+ ancestors.add(location);
+ }
+
+ private LinkedList addAndCreateList(LinkedList list, LocationWrapper toAdd ){
+ if ( list == null ) {
+ list = new LinkedList<>();
+ }
+ list.add( toAdd );
+ return list;
+ }
+
+ /**
+ * This method is used to find all possible LocationWrappers of a file for a specific task.
+ * @return a list of all LocationWrapper of this file that could be used and a list of all Locations that are in use and are not in a version that this task could use.
+ */
+ public MatchingLocationsPair getFilesForTask( Task task ) throws NoAlignmentFoundException {
+ LocationWrapper[] locationsRef = this.locations;
+
+ LinkedList current = null;
+ LinkedList ancestors = null;
+ LinkedList descendants = null;
+ LinkedList unrelated = null;
+ LinkedList initial = null;
+
+ final Process taskProcess = task.getProcess();
+ final Set taskAncestors = taskProcess.getAncestors();
+ final Set taskDescendants = taskProcess.getDescendants();
+
+ Set inUse = null;
+
+ for ( LocationWrapper location : locationsRef ) {
+
+ if( location.isInUse() ) {
+ if ( inUse == null ) {
+ inUse = new HashSet<>();
+ }
+ inUse.add(location.getLocation());
+ }
+
+ if ( !location.isActive() ) {
+ continue;
+ }
+
+ //File was modified by an operator (no relation known)
+ if ( location.getCreatedByTask() == null ) {
+ initial = addAndCreateList( initial, location );
+ continue;
+ }
+
+ final Process locationProcess = location.getCreatedByTask().getProcess();
+ if ( locationProcess == taskProcess)
+ //Location was created by the same process == does definitely fit.
+ {
+ current = addAndCreateList( current, location );
+ } else if ( taskAncestors.contains(locationProcess) ) {
+ // location is a direct ancestor
+ if ( ancestors == null ) {
+ ancestors = new LinkedList<>();
+ ancestors.add( location );
+ } else {
+ addToAncestors( ancestors, location, locationProcess );
+ }
+ }
+ else if ( taskDescendants.contains(locationProcess) )
+ // location is a direct descendant
+ {
+ descendants = addAndCreateList( descendants, location );
+ } else
+ // location was possibly generated in parallel
+ {
+ unrelated = addAndCreateList( unrelated, location );
+ }
+ }
+
+ final List matchingLocations = ( initial == null )
+ ? combineResultsEmptyInitial( current, ancestors, descendants, unrelated )
+ : combineResultsWithInitial( current, ancestors, descendants, unrelated, initial );
+
+ if ( matchingLocations == null ) {
+ throw new NoAlignmentFoundException();
+ }
+ removeMatchingLocations( matchingLocations, inUse );
+
+ return new MatchingLocationsPair( matchingLocations, inUse );
+ }
+
+ private void removeMatchingLocations( List matchingLocations, Set locations ){
+ if ( locations == null || matchingLocations == null ) {
+ return;
+ }
+ for ( LocationWrapper matchingLocation : matchingLocations ) {
+ if( matchingLocation.isInUse() ) {
+ locations.remove(matchingLocation.getLocation());
+ }
+ }
+ }
+
+ @Getter
+ public class MatchingLocationsPair {
+
+ private final List matchingLocations;
+ private final Set excludedNodes;
+
+ private MatchingLocationsPair(List matchingLocations, Set excludedNodes) {
+ this.matchingLocations = matchingLocations;
+ this.excludedNodes = excludedNodes;
+ }
+
+ }
+
+ private void addAllLaterLocationsToResult( List list, List result, long time ){
+ if( list != null ) {
+ for ( LocationWrapper l : list ) {
+ if( l.getCreateTime() >= time ) {
+ result.add( l );
+ }
+ }
+ }
+ }
+
+ public LocationWrapper getLastUpdate( LocationType type ){
+ LocationWrapper lastLocation = null;
+ for (LocationWrapper location : locations) {
+ if( location.isActive() && location.getLocation().getType() == type && (lastLocation == null || lastLocation.getCreateTime() < location.getCreateTime() )){
+ lastLocation = location;
+ }
+ }
+ return lastLocation;
+ }
+
+ public LocationWrapper getLocationWrapper( Location location ){
+ for (LocationWrapper locationWrapper : locations) {
+ if ( locationWrapper.isActive() && locationWrapper.getLocation() == location ) {
+ return locationWrapper;
+ }
+ }
+ throw new RuntimeException( "Not found: " + location.getIdentifier() );
+ }
+
+ public void requestedByTask(){
+ wasRequestedByTask = true;
+ }
+
+ public boolean wasRequestedByTask(){
+ return wasRequestedByTask;
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/outfiles/OutputFile.java b/src/main/java/cws/k8s/scheduler/model/outfiles/OutputFile.java
new file mode 100644
index 00000000..2e43afc4
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/outfiles/OutputFile.java
@@ -0,0 +1,31 @@
+package cws.k8s.scheduler.model.outfiles;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.nio.file.Path;
+import java.util.Objects;
+
+@Getter
+@RequiredArgsConstructor
+public class OutputFile {
+
+ private final Path path;
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof OutputFile)) {
+ return false;
+ }
+ OutputFile that = (OutputFile) o;
+ return Objects.equals(getPath(), that.getPath());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getPath());
+ }
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/outfiles/PathLocationWrapperPair.java b/src/main/java/cws/k8s/scheduler/model/outfiles/PathLocationWrapperPair.java
new file mode 100644
index 00000000..1465f079
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/outfiles/PathLocationWrapperPair.java
@@ -0,0 +1,47 @@
+package cws.k8s.scheduler.model.outfiles;
+
+import cws.k8s.scheduler.model.location.hierachy.LocationWrapper;
+import lombok.Getter;
+
+import java.nio.file.Path;
+import java.util.Objects;
+
+@Getter
+public class PathLocationWrapperPair extends OutputFile {
+
+ private final LocationWrapper locationWrapper;
+
+ public PathLocationWrapperPair(Path path, LocationWrapper locationWrapper) {
+ super( path );
+ this.locationWrapper = locationWrapper;
+ }
+
+ @Override
+ public String toString() {
+ return "PathLocationWrapperPair{" +
+ "path=" + getPath() +
+ ", locationWrapper=" + locationWrapper +
+ '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof PathLocationWrapperPair)) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+ PathLocationWrapperPair that = (PathLocationWrapperPair) o;
+ return Objects.equals(getLocationWrapper(), that.getLocationWrapper());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), getLocationWrapper());
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/outfiles/SymlinkOutput.java b/src/main/java/cws/k8s/scheduler/model/outfiles/SymlinkOutput.java
new file mode 100644
index 00000000..d3345f73
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/outfiles/SymlinkOutput.java
@@ -0,0 +1,43 @@
+package cws.k8s.scheduler.model.outfiles;
+
+import lombok.Getter;
+
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Objects;
+
+@Getter
+public class SymlinkOutput extends OutputFile {
+
+ private final Path dst;
+
+ public SymlinkOutput( String path, String dst ) {
+ super( Paths.get(path) );
+ this.dst = Paths.get(dst);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof SymlinkOutput)) {
+ return false;
+ }
+ if (!super.equals(o)) {
+ return false;
+ }
+ SymlinkOutput that = (SymlinkOutput) o;
+ return Objects.equals(dst, that.dst);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(super.hashCode(), dst);
+ }
+
+ @Override
+ public String toString() {
+ return "SymlinkOutput{" + super.getPath() + " -> " + getDst() + "}";
+ }
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/taskinputs/Input.java b/src/main/java/cws/k8s/scheduler/model/taskinputs/Input.java
new file mode 100644
index 00000000..a1b71c29
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/taskinputs/Input.java
@@ -0,0 +1,4 @@
+package cws.k8s.scheduler.model.taskinputs;
+
+public interface Input {
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/taskinputs/PathFileLocationTriple.java b/src/main/java/cws/k8s/scheduler/model/taskinputs/PathFileLocationTriple.java
new file mode 100644
index 00000000..aab7cdcf
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/taskinputs/PathFileLocationTriple.java
@@ -0,0 +1,66 @@
+package cws.k8s.scheduler.model.taskinputs;
+
+import cws.k8s.scheduler.model.location.Location;
+import cws.k8s.scheduler.model.location.hierachy.LocationWrapper;
+import cws.k8s.scheduler.model.location.hierachy.RealHierarchyFile;
+import lombok.EqualsAndHashCode;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+
+import java.nio.file.Path;
+import java.util.List;
+
+@ToString( exclude = "file" )
+@EqualsAndHashCode
+@RequiredArgsConstructor
+public class PathFileLocationTriple implements Input {
+
+ public final Path path;
+ public final RealHierarchyFile file;
+ public final List locations;
+ private long size = -1;
+
+ public long getSizeInBytes() {
+ if ( this.size != -1 ) {
+ return this.size;
+ }
+ long currentSize = 0;
+ for (LocationWrapper location : locations) {
+ currentSize += location.getSizeInBytes();
+ }
+ this.size = currentSize / locations.size();
+ return this.size;
+ }
+
+ public long getMinSizeInBytes() {
+ if ( locations.isEmpty() ) {
+ throw new IllegalStateException("No locations for file " + path);
+ }
+ long minSize = Long.MAX_VALUE;
+ for (LocationWrapper location : locations) {
+ if ( location.getSizeInBytes() < minSize ) {
+ minSize = location.getSizeInBytes();
+ }
+ }
+ return minSize;
+ }
+
+ public LocationWrapper locationWrapperOnLocation(Location loc){
+ for (LocationWrapper location : locations) {
+ if ( location.getLocation().equals(loc) ) {
+ return location;
+ }
+ }
+ throw new IllegalStateException("LocationWrapper not found for location " + loc);
+ }
+
+ public boolean locatedOnLocation(Location loc){
+ for (LocationWrapper location : locations) {
+ if ( location.getLocation() == loc ) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/taskinputs/SymlinkInput.java b/src/main/java/cws/k8s/scheduler/model/taskinputs/SymlinkInput.java
new file mode 100644
index 00000000..cc08e6dd
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/taskinputs/SymlinkInput.java
@@ -0,0 +1,20 @@
+package cws.k8s.scheduler.model.taskinputs;
+
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+
+import java.nio.file.Path;
+
+@Getter
+@EqualsAndHashCode
+public class SymlinkInput implements Input {
+
+ private final String src;
+ private final String dst;
+
+ public SymlinkInput(Path src, Path dst) {
+ this.src = src.toAbsolutePath().toString();
+ this.dst = dst.toAbsolutePath().toString();
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/taskinputs/TaskInputs.java b/src/main/java/cws/k8s/scheduler/model/taskinputs/TaskInputs.java
new file mode 100644
index 00000000..72121d4b
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/model/taskinputs/TaskInputs.java
@@ -0,0 +1,125 @@
+package cws.k8s.scheduler.model.taskinputs;
+
+import cws.k8s.scheduler.model.location.Location;
+import cws.k8s.scheduler.model.location.hierachy.LocationWrapper;
+import cws.k8s.scheduler.util.TaskNodeStats;
+import cws.k8s.scheduler.util.Tuple;
+import cws.k8s.scheduler.util.copying.CopySource;
+import cws.k8s.scheduler.util.copying.CurrentlyCopyingOnNode;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+@ToString
+@Slf4j
+@RequiredArgsConstructor
+public class TaskInputs {
+
+ @Getter
+ private final List symlinks;
+ @Getter
+ private final List files;
+ private final Set excludedNodes;
+
+ private boolean sorted = false;
+
+ public boolean canRunOnLoc( Location loc ) {
+ return !excludedNodes.contains( loc );
+ }
+
+ public boolean hasExcludedNodes() {
+ return !excludedNodes.isEmpty();
+ }
+
+ public boolean allFilesAreOnLocationAndNotOverwritten( Location loc, Set pathCurrentlyCopying ){
+ for (PathFileLocationTriple file : files) {
+ if ( !file.locatedOnLocation(loc) || (pathCurrentlyCopying != null && pathCurrentlyCopying.contains(file.path.toString())) ) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public List allLocationWrapperOnLocation( Location loc){
+ return files
+ .parallelStream()
+ .map( file -> file.locationWrapperOnLocation( loc ) )
+ .collect( Collectors.toList() );
+ }
+
+ public long calculateDataOnNode( Location loc ) {
+ return calculateDataOnNodeAdditionalInfo( loc ).getB();
+ }
+
+ /**
+ * Calculates the data on a node and returns whether all data is on the location
+ * @return boolean: true if all files are on location, Long: data on location
+ */
+ public Tuple calculateDataOnNodeAdditionalInfo( Location loc ) {
+ long size = 0;
+ boolean allOnNode = true;
+ for ( PathFileLocationTriple fileLocation : files ) {
+ if (fileLocation.locatedOnLocation(loc)) {
+ size += fileLocation.getSizeInBytes();
+ } else {
+ allOnNode = false;
+ }
+ }
+ return new Tuple<>( allOnNode, size );
+ }
+
+ /**
+ * Calculates the data on a node and returns whether all data is on the location
+ * @return the size remaining and the amount of data currently copying. Null if the task cannot run on this node.
+ */
+ public TaskNodeStats calculateMissingData( Location loc, CurrentlyCopyingOnNode currentlyCopying ) {
+ long sizeRemaining = 0;
+ long sizeCurrentlyCopying = 0;
+ long sizeOnNode = 0;
+ for ( PathFileLocationTriple fileLocation : files ) {
+ final long minSizeInBytes = fileLocation.getMinSizeInBytes();
+ //Is the file already on the node?
+ if ( fileLocation.locatedOnLocation(loc) ) {
+ sizeOnNode += minSizeInBytes;
+ } else {
+ //is the file currently copying?
+ final CopySource copySource = currentlyCopying.getCopySource( fileLocation.path.toString() );
+ if ( copySource != null ) {
+ //Is this file compatible with the task?
+ if ( fileLocation.locatedOnLocation( copySource.getLocation() ) ) {
+ sizeCurrentlyCopying += minSizeInBytes;
+ } else {
+ //currently copying file is incompatible with this task
+ return null;
+ }
+ } else {
+ sizeRemaining += minSizeInBytes;
+ }
+ }
+ }
+ return new TaskNodeStats( sizeRemaining, sizeCurrentlyCopying, sizeOnNode );
+ }
+
+ public long calculateAvgSize() {
+ long size = 0;
+ for ( PathFileLocationTriple file : files ) {
+ size += file.getSizeInBytes();
+ }
+ return size;
+ }
+
+ public void sort(){
+ synchronized ( files ) {
+ if (!sorted) {
+ files.sort((x, y) -> Long.compare(y.getSizeInBytes(), x.getSizeInBytes()));
+ sorted = true;
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/model/tracing/TraceRecord.java b/src/main/java/cws/k8s/scheduler/model/tracing/TraceRecord.java
index c44d7988..9e331397 100644
--- a/src/main/java/cws/k8s/scheduler/model/tracing/TraceRecord.java
+++ b/src/main/java/cws/k8s/scheduler/model/tracing/TraceRecord.java
@@ -6,12 +6,40 @@
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
+import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class TraceRecord {
+ @Getter
+ @Setter
+ /*Filesize required for task*/
+ private Long schedulerFilesBytes = null;
+
+ @Getter
+ @Setter
+ /*Filesize required for task and already on node*/
+ private Long schedulerFilesNodeBytes = null;
+
+ @Getter
+ @Setter
+ /*Filesize required for task and already copied by other task*/
+ private Long schedulerFilesNodeOtherTaskBytes = null;
+
+ @Getter
+ @Setter
+ private Integer schedulerFiles = null;
+
+ @Getter
+ @Setter
+ private Integer schedulerFilesNode = null;
+
+ @Getter
+ @Setter
+ private Integer schedulerFilesNodeOtherTask = null;
+
@Getter
@Setter
private Integer schedulerDependingTask = null;
@@ -24,6 +52,10 @@ public class TraceRecord {
@Setter
private Integer schedulerPlaceInQueue = null;
+ @Getter
+ @Setter
+ private Integer schedulerLocationCount = null;
+
@Getter
@Setter
private Integer schedulerNodesTried = null;
@@ -42,10 +74,18 @@ public class TraceRecord {
private int schedulerTriedToSchedule = 0;
+ @Getter
+ @Setter
+ private Integer schedulerNodesToCopyFrom = null;
+
@Getter
@Setter
private Integer schedulerTimeToSchedule = null;
+ @Getter
+ @Setter
+ private Integer schedulerNoAlignmentFound = null;
+
private Integer schedulerDeltaScheduleSubmitted = null;
private Integer schedulerDeltaScheduleAlignment = null;
@@ -75,19 +115,40 @@ public class TraceRecord {
/*Time delta between a task was submitted and the batch became schedulable*/
private Integer schedulerDeltaSubmittedBatchEnd = null;
+ @Getter
+ private List schedulerTimeDeltaPhaseThree = null;
+
+ private int schedulerCopyTasks = 0;
+
+ public void addSchedulerTimeDeltaPhaseThree( Integer schedulerTimeDeltaPhaseThree ) {
+ if ( this.schedulerTimeDeltaPhaseThree == null ) {
+ this.schedulerTimeDeltaPhaseThree = new ArrayList<>();
+ }
+ this.schedulerTimeDeltaPhaseThree.add( schedulerTimeDeltaPhaseThree );
+ }
+
public void writeRecord( String tracePath ) throws IOException {
try ( BufferedWriter bw = new BufferedWriter( new FileWriter( tracePath ) ) ) {
bw.write("nextflow.scheduler.trace/v1\n");
+ writeValue("scheduler_files_bytes", schedulerFilesBytes, bw);
+ writeValue("scheduler_files_node_bytes", schedulerFilesNodeBytes, bw);
+ writeValue("scheduler_files_node_other_task_bytes", schedulerFilesNodeOtherTaskBytes, bw);
+ writeValue("scheduler_files", schedulerFiles, bw);
+ writeValue("scheduler_files_node", schedulerFilesNode, bw);
+ writeValue("scheduler_files_node_other_task", schedulerFilesNodeOtherTask, bw);
writeValue("scheduler_depending_task", schedulerDependingTask, bw);
writeValue("scheduler_time_in_queue", schedulerTimeInQueue, bw);
writeValue("scheduler_place_in_queue", schedulerPlaceInQueue, bw);
+ writeValue("scheduler_location_count", schedulerLocationCount, bw);
writeValue("scheduler_nodes_tried", schedulerNodesTried, bw);
writeValue("scheduler_nodes_cost", schedulerNodesCost, bw);
writeValue("scheduler_could_stop_fetching", schedulerCouldStopFetching, bw);
writeValue("scheduler_best_cost", schedulerBestCost, bw);
writeValue("scheduler_tried_to_schedule", schedulerTriedToSchedule, bw);
+ writeValue("scheduler_nodes_to_copy_from", schedulerNodesToCopyFrom, bw);
writeValue("scheduler_time_to_schedule", schedulerTimeToSchedule, bw);
+ writeValue("scheduler_no_alignment_found", schedulerNoAlignmentFound, bw);
writeValue("scheduler_delta_schedule_submitted", schedulerDeltaScheduleSubmitted, bw);
writeValue("scheduler_delta_schedule_alignment", schedulerDeltaScheduleAlignment, bw);
writeValue("scheduler_batch_id", schedulerBatchId, bw);
@@ -95,7 +156,8 @@ public void writeRecord( String tracePath ) throws IOException {
writeValue("scheduler_delta_batch_start_received", schedulerDeltaBatchStartReceived, bw);
writeValue("scheduler_delta_batch_closed_batch_end", schedulerDeltaBatchClosedBatchEnd, bw);
writeValue("scheduler_delta_submitted_batch_end", schedulerDeltaSubmittedBatchEnd, bw);
-
+ writeValue("scheduler_time_delta_phase_three", schedulerTimeDeltaPhaseThree, bw);
+ writeValue("scheduler_copy_tasks", schedulerCopyTasks, bw);
}
}
@@ -106,7 +168,7 @@ private void writeValue( String name, T value, BufferedWriter
}
}
- private void writeValue( String name, List value, BufferedWriter bw ) throws IOException {
+ private void writeValue( String name, List extends Number> value, BufferedWriter bw ) throws IOException {
if ( value != null ) {
final String collect = value.stream()
.map( x -> x==null ? "null" : x.toString() )
@@ -136,5 +198,9 @@ public void tryToSchedule( long startSchedule ){
schedulerTriedToSchedule++;
}
+ public void copyTask(){
+ schedulerCopyTasks++;
+ }
+
}
diff --git a/src/main/java/cws/k8s/scheduler/prediction/offset/VarianceOffset.java b/src/main/java/cws/k8s/scheduler/prediction/offset/VarianceOffset.java
index 908d0597..6f368251 100644
--- a/src/main/java/cws/k8s/scheduler/prediction/offset/VarianceOffset.java
+++ b/src/main/java/cws/k8s/scheduler/prediction/offset/VarianceOffset.java
@@ -2,8 +2,6 @@
import cws.k8s.scheduler.model.Task;
import cws.k8s.scheduler.prediction.Predictor;
-import org.apache.commons.math3.stat.descriptive.moment.Variance;
-import org.apache.commons.math3.stat.descriptive.rank.Percentile;
import java.util.List;
diff --git a/src/main/java/cws/k8s/scheduler/prediction/predictor/LinearPredictor.java b/src/main/java/cws/k8s/scheduler/prediction/predictor/LinearPredictor.java
index bf044daa..64e0412e 100644
--- a/src/main/java/cws/k8s/scheduler/prediction/predictor/LinearPredictor.java
+++ b/src/main/java/cws/k8s/scheduler/prediction/predictor/LinearPredictor.java
@@ -1,6 +1,5 @@
package cws.k8s.scheduler.prediction.predictor;
-import cws.k8s.scheduler.model.Task;
import cws.k8s.scheduler.prediction.Predictor;
public interface LinearPredictor extends Predictor {
diff --git a/src/main/java/cws/k8s/scheduler/prediction/predictor/LinearPredictorCustomLoss.java b/src/main/java/cws/k8s/scheduler/prediction/predictor/LinearPredictorCustomLoss.java
index 0f74a4fe..27c93ee8 100644
--- a/src/main/java/cws/k8s/scheduler/prediction/predictor/LinearPredictorCustomLoss.java
+++ b/src/main/java/cws/k8s/scheduler/prediction/predictor/LinearPredictorCustomLoss.java
@@ -1,10 +1,8 @@
package cws.k8s.scheduler.prediction.predictor;
import cws.k8s.scheduler.model.Task;
-import cws.k8s.scheduler.prediction.Predictor;
import cws.k8s.scheduler.prediction.extractor.VariableExtractor;
import cws.k8s.scheduler.prediction.predictor.loss.UnequalLossFunction;
-import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.math3.optim.InitialGuess;
import org.apache.commons.math3.optim.MaxEval;
diff --git a/src/main/java/cws/k8s/scheduler/prediction/predictor/LinearPredictorSquaredLoss.java b/src/main/java/cws/k8s/scheduler/prediction/predictor/LinearPredictorSquaredLoss.java
index e427b680..459c4143 100644
--- a/src/main/java/cws/k8s/scheduler/prediction/predictor/LinearPredictorSquaredLoss.java
+++ b/src/main/java/cws/k8s/scheduler/prediction/predictor/LinearPredictorSquaredLoss.java
@@ -1,7 +1,6 @@
package cws.k8s.scheduler.prediction.predictor;
import cws.k8s.scheduler.model.Task;
-import cws.k8s.scheduler.prediction.Predictor;
import cws.k8s.scheduler.prediction.extractor.VariableExtractor;
import lombok.RequiredArgsConstructor;
import org.apache.commons.math3.stat.regression.SimpleRegression;
diff --git a/src/main/java/cws/k8s/scheduler/prediction/predictor/MeanPredictor.java b/src/main/java/cws/k8s/scheduler/prediction/predictor/MeanPredictor.java
index 5243a05a..710fbb08 100644
--- a/src/main/java/cws/k8s/scheduler/prediction/predictor/MeanPredictor.java
+++ b/src/main/java/cws/k8s/scheduler/prediction/predictor/MeanPredictor.java
@@ -4,7 +4,6 @@
import cws.k8s.scheduler.prediction.Predictor;
import cws.k8s.scheduler.prediction.extractor.VariableExtractor;
import lombok.RequiredArgsConstructor;
-import org.apache.commons.math3.stat.regression.SimpleRegression;
import java.util.concurrent.atomic.AtomicLong;
diff --git a/src/main/java/cws/k8s/scheduler/rest/PathAttributes.java b/src/main/java/cws/k8s/scheduler/rest/PathAttributes.java
new file mode 100644
index 00000000..fafc03b7
--- /dev/null
+++ b/src/main/java/cws/k8s/scheduler/rest/PathAttributes.java
@@ -0,0 +1,18 @@
+package cws.k8s.scheduler.rest;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.ToString;
+
+@Getter
+@ToString
+@NoArgsConstructor( access = AccessLevel.PRIVATE, force = true )
+public class PathAttributes {
+
+ private final String path;
+ private final long size;
+ private final long timestamp;
+ private final long locationWrapperID;
+
+}
diff --git a/src/main/java/cws/k8s/scheduler/rest/SchedulerRestController.java b/src/main/java/cws/k8s/scheduler/rest/SchedulerRestController.java
index 2012e7d7..24cfdf22 100644
--- a/src/main/java/cws/k8s/scheduler/rest/SchedulerRestController.java
+++ b/src/main/java/cws/k8s/scheduler/rest/SchedulerRestController.java
@@ -1,14 +1,19 @@
package cws.k8s.scheduler.rest;
+import cws.k8s.scheduler.client.CWSKubernetesClient;
import cws.k8s.scheduler.dag.DAG;
import cws.k8s.scheduler.dag.InputEdge;
-import cws.k8s.scheduler.client.CWSKubernetesClient;
import cws.k8s.scheduler.dag.Vertex;
import cws.k8s.scheduler.model.SchedulerConfig;
import cws.k8s.scheduler.model.TaskConfig;
import cws.k8s.scheduler.model.TaskMetrics;
-import cws.k8s.scheduler.scheduler.PrioritizeAssignScheduler;
-import cws.k8s.scheduler.scheduler.Scheduler;
+import cws.k8s.scheduler.rest.exceptions.NotARealFileException;
+import cws.k8s.scheduler.rest.response.getfile.FileResponse;
+import cws.k8s.scheduler.scheduler.*;
+import cws.k8s.scheduler.scheduler.filealignment.GreedyAlignment;
+import cws.k8s.scheduler.scheduler.filealignment.costfunctions.CostFunction;
+import cws.k8s.scheduler.scheduler.filealignment.costfunctions.MinSizeCost;
+import cws.k8s.scheduler.scheduler.la2.ready2run.OptimalReadyToRunToNode;
import cws.k8s.scheduler.scheduler.nodeassign.FairAssign;
import cws.k8s.scheduler.scheduler.nodeassign.NodeAssign;
import cws.k8s.scheduler.scheduler.nodeassign.RandomNodeAssign;
@@ -18,6 +23,7 @@
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
@@ -29,6 +35,7 @@
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.web.bind.annotation.*;
+import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -91,7 +98,8 @@ private ResponseEntity noSchedulerFor( String execution ){
@PostMapping("/v1/scheduler/{execution}")
ResponseEntity registerScheduler(
@PathVariable String execution,
- @RequestBody(required = false) SchedulerConfig config
+ @RequestBody(required = false) SchedulerConfig config,
+ HttpServletRequest request
) {
final String namespace = config.namespace;
@@ -105,7 +113,37 @@ ResponseEntity registerScheduler(
return noSchedulerFor( execution );
}
+ CostFunction costFunction = null;
+ if ( config.costFunction != null ) {
+ switch (config.costFunction.toLowerCase()) {
+ case "minsize": costFunction = new MinSizeCost(0); break;
+ default:
+ log.warn( "Register execution: {} - No cost function for: {}", execution, config.costFunction );
+ return new ResponseEntity<>( "No cost function: " + config.costFunction, HttpStatus.NOT_FOUND );
+ }
+ }
+
switch ( strategy.toLowerCase() ){
+ case "wow" :
+ if ( !config.locationAware ) {
+ log.warn( "Register execution: {} - LA scheduler only works if location aware", execution );
+ return new ResponseEntity<>( "LA scheduler only works if location aware", HttpStatus.BAD_REQUEST );
+ }
+ if ( costFunction == null ) {
+ costFunction = new MinSizeCost( 0 );
+ }
+ scheduler = new LocationAwareSchedulerV2( execution, client, namespace, config, new GreedyAlignment( 0.5, costFunction ), new OptimalReadyToRunToNode() );
+ break;
+ case "wowgroup" :
+ if ( !config.locationAware ) {
+ log.warn( "Register execution: {} - LA scheduler only works if location aware", execution );
+ return new ResponseEntity<>( "LA scheduler only works if location aware", HttpStatus.BAD_REQUEST );
+ }
+ if ( costFunction == null ) {
+ costFunction = new MinSizeCost( 0 );
+ }
+ scheduler = new LocationAwareSchedulerGroups( execution, client, namespace, config, new GreedyAlignment( 0.5, costFunction ), new OptimalReadyToRunToNode() );
+ break;
default: {
final String[] split = strategy.split( "-" );
Prioritize prioritize;
@@ -124,6 +162,7 @@ ResponseEntity registerScheduler(
case "max": prioritize = new MaxInputPrioritize(); break;
case "min": prioritize = new MinInputPrioritize(); break;
default:
+ log.warn( "Register execution: {} - No Prioritize for: {}", execution, split[0] );
return new ResponseEntity<>( "No Prioritize for: " + split[0], HttpStatus.NOT_FOUND );
}
if ( split.length == 2 ) {
@@ -132,6 +171,7 @@ ResponseEntity registerScheduler(
case "roundrobin": case "rr": assign = new RoundRobinAssign(); break;
case "fair": case "f": assign = new FairAssign(); break;
default:
+ log.warn( "Register execution: {} - No Assign for: {}", execution, split[1] );
return new ResponseEntity<>( "No Assign for: " + split[1], HttpStatus.NOT_FOUND );
}
} else {
@@ -139,10 +179,14 @@ ResponseEntity registerScheduler(
}
scheduler = new PrioritizeAssignScheduler( execution, client, namespace, config, prioritize, assign );
} else {
+ log.warn( "Register execution: {} - No scheduler for strategy: {}", execution, strategy );
return new ResponseEntity<>( "No scheduler for strategy: " + strategy, HttpStatus.NOT_FOUND );
}
}
}
+ if ( scheduler instanceof SchedulerWithDaemonSet ) {
+ ((SchedulerWithDaemonSet) scheduler).setWorkflowEngineNode( request.getRemoteAddr() );
+ }
schedulerHolder.put( execution, scheduler );
client.addInformable( scheduler );
@@ -167,7 +211,7 @@ ResponseEntity registerScheduler(
@PostMapping("/v1/scheduler/{execution}/task/{id}")
ResponseEntity extends Object> registerTask( @PathVariable String execution, @PathVariable int id, @RequestBody TaskConfig config ) {
- log.trace( execution + " " + config.getTask() + " got: " + config );
+ log.info( execution + " " + config.getTask() + " got: " + config );
final Scheduler scheduler = schedulerHolder.get( execution );
if ( scheduler == null ) {
@@ -322,6 +366,98 @@ ResponseEntity delete( @PathVariable String execution ) {
return new ResponseEntity<>( HttpStatus.OK );
}
+ @GetMapping("/v1/daemon/{execution}/{node}")
+ ResponseEntity getDaemonName( @PathVariable String execution, @PathVariable String node ) {
+
+ log.info( "Asking for Daemon exec: {} node: {}", execution, node );
+
+ final Scheduler scheduler = schedulerHolder.get( execution );
+ if ( !(scheduler instanceof SchedulerWithDaemonSet) ) {
+ return noSchedulerFor( execution );
+ }
+
+ String daemon = ((SchedulerWithDaemonSet) scheduler).getDaemonIpOnNode( node );
+
+ if ( daemon == null ){
+ return new ResponseEntity<>( "No daemon for node found: " + node , HttpStatus.NOT_FOUND );
+ }
+
+ return new ResponseEntity<>( daemon, HttpStatus.OK );
+
+ }
+
+ @PostMapping("/v1/downloadtask/{execution}")
+ ResponseEntity finishDownload( @PathVariable String execution, @RequestBody byte[] task ) {
+
+ String name = new String( task, StandardCharsets.UTF_8 );
+ if ( name.endsWith( "=" ) ) {
+ name = name.substring( 0, name.length() - 1 );
+ }
+
+ final Scheduler scheduler = schedulerHolder.get( execution );
+ if ( !(scheduler instanceof SchedulerWithDaemonSet) ) {
+ return noSchedulerFor( execution );
+ }
+ ((SchedulerWithDaemonSet) scheduler).taskHasFinishedCopyTask( name );
+ return new ResponseEntity<>( HttpStatus.OK );
+
+ }
+
+ @GetMapping("/v1/file/{execution}")
+ ResponseEntity extends Object> getNodeForFile( @PathVariable String execution, @RequestParam String path ) {
+
+ log.info( "Get file location request: {} {}", execution, path );
+
+ final Scheduler scheduler = schedulerHolder.get( execution );
+ if ( !(scheduler instanceof SchedulerWithDaemonSet) ) {
+ return noSchedulerFor( execution );
+ }
+
+ FileResponse fileResponse;
+ try {
+ fileResponse = ((SchedulerWithDaemonSet) scheduler).nodeOfLastFileVersion( path );
+ log.info(fileResponse.toString());
+ } catch (NotARealFileException e) {
+ return new ResponseEntity<>( "Requested path is not a real file: " + path , HttpStatus.BAD_REQUEST );
+ }
+
+ if ( fileResponse == null ){
+ return new ResponseEntity<>( "No node for file found: " + path , HttpStatus.NOT_FOUND );
+ }
+
+ return new ResponseEntity<>( fileResponse, HttpStatus.OK );
+
+ }
+
+ @PostMapping("/v1/file/{execution}/location/{method}")
+ ResponseEntity changeLocationForFile( @PathVariable String method, @PathVariable String execution, @RequestBody PathAttributes pa ) {
+ return changeLocationForFile( method, execution, null, pa );
+ }
+
+ @PostMapping("/v1/file/{execution}/location/{method}/{node}")
+ ResponseEntity changeLocationForFile( @PathVariable String method, @PathVariable String execution, @PathVariable String node, @RequestBody PathAttributes pa ) {
+
+ log.info( "Change file location request: {} {} {}", method, execution, pa );
+
+ final Scheduler scheduler = schedulerHolder.get( execution );
+ if ( !(scheduler instanceof SchedulerWithDaemonSet) ) {
+ log.info( "No scheduler for: " + execution );
+ return noSchedulerFor( execution );
+ }
+
+ if ( !method.equals("add") && !method.equals("overwrite") ) {
+ log.info("Method not found: " + method);
+ return new ResponseEntity<>( "Method not found: " + method , HttpStatus.NOT_FOUND );
+ }
+
+ boolean overwrite = method.equals("overwrite");
+
+ ((SchedulerWithDaemonSet) scheduler).addFile( pa.getPath(), pa.getSize(), pa.getTimestamp(), pa.getLocationWrapperID(), overwrite, node );
+
+ return new ResponseEntity<>( HttpStatus.OK );
+
+ }
+
@GetMapping ("/health")
ResponseEntity