Skip to content

Commit

Permalink
Merge pull request #231 from TU-Wien-dataLAB/200-celery
Browse files Browse the repository at this point in the history
Celery
  • Loading branch information
meffmadd authored Sep 2, 2024
2 parents d73ec4a + f431ce8 commit 97513fd
Show file tree
Hide file tree
Showing 23 changed files with 450 additions and 361 deletions.
10 changes: 7 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,12 @@ Getting Started

.. running-start
Grader service uses RabbitMQ as a task broker to delegate grading tasks to separate worker instances.
Please follow their `tutorials <https://www.rabbitmq.com/docs/download>`_ on how to set up and run a RabbitMQ server on your host machine.
Our `helm` chart automatically deploys a RabbitMQ cluster when installing the grader service through the `RabbitMQ Kubernetes Operator <https://www.rabbitmq.com/docs/kubernetes/operator/operator-overview>`_.

Running grader service
=======================
--------------------------

To run the grader service you first have to register the service in JupyterHub as an unmanaged service in the config:

Expand All @@ -160,7 +164,7 @@ You can verify the config by running ``jupyterhub -f <config_file.py>`` and you
Cannot connect to external service grader at http://127.0.0.1:4010. Is it running?

Specifying user roles
======================
--------------------------

Since the JupyterHub is the only source of authentication for the service, it has to rely on the JupyterHub to provide all the necessary information for user groups.

Expand Down Expand Up @@ -195,7 +199,7 @@ The config could look like this:
Here, ``user1`` is an instructor of the lecture with the code ``lect1`` and so on.

Starting the service
=====================
--------------------------

In order to start the grader service we have to provide a configuration file for it as well:

Expand Down
79 changes: 79 additions & 0 deletions charts/grader-service/templates/celery-worker-deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "grader-service.fullname" . }}-worker
labels:
{{- include "grader-service.labels" . | nindent 4 }}
namespace: {{ .Release.Namespace }}
spec:
replicas: {{ .Values.workers.replication }}
selector:
matchLabels:
{{- include "grader-service.selectorLabels" . | nindent 6 }}
template:
metadata:
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "grader-service.selectorLabels" . | nindent 8 }}
hub.jupyter.org/network-access-hub: "true"
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "grader-service.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
containers:
- name: {{ .Chart.Name }}-worker
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command: [ "grader-worker" ]
args: [ "-f", "/etc/grader-service/grader_service_config.py" ]
resources:
{{- toYaml .Values.workers.resources | nindent 12 }}
env:
- name: GRADER_SERVICE_PORT
value: {{ .Values.port | quote }}
- name: RABBITMQ_GRADER_SERVICE_USERNAME
valueFrom:
secretKeyRef:
key: username
name: rabbitmq-grader-service-default-user
- name: RABBITMQ_GRADER_SERVICE_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: rabbitmq-grader-service-default-user
volumeMounts:
- name: data
mountPath: /var/lib/grader-service
{{- if .Values.subPath }}
subPath: {{ .Values.subPath }}
{{- end }}
- name: config
mountPath: /etc/grader-service/grader_service_config.py
subPath: grader_service_config.py
- name: config
mountPath: /var/lib/grader-service/.gitconfig
subPath: .gitconfig
volumes:
- name: data
{{- if .Values.hostpath }}
hostPath:
path: {{ .Values.hostpath }}
type: DirectoryOrCreate
{{- else }}
persistentVolumeClaim:
claimName: grader-service
readOnly: false
{{- end }}
- name: config
configMap:
defaultMode: 444
name: grader-service
18 changes: 14 additions & 4 deletions charts/grader-service/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,11 @@ data:
{{- if .Values.requestHandlerConfig }}
{{- if .Values.requestHandlerConfig.git_max_file_mb }}
c.RequestHandlerConfig.git_max_file_size_mb = {{ .Values.requestHandlerConfig.git_max_file_mb }}
{{- end }}
{{- end }}
{{- end }}

c.GraderExecutor.n_concurrent_tasks = {{ .Values.concurrentGradingTasks | int }}

c.KubeAutogradeExecutor.kube_context = None

{{- if .Values.kubeAutogradeExecutor.namespace }}
c.KubeAutogradeExecutor.namespace = {{ .Values.kubeAutogradeExecutor.namespace | quote }}
{{- end }}
Expand All @@ -45,6 +43,18 @@ data:
c.GraderService.max_buffer_size = {{ .Values.requestHandlerConfig.max_buffer_size | int }}
c.GraderService.max_body_size = {{ .Values.requestHandlerConfig.max_body_size | int }}

import os
broker_url=f'amqp://{os.getenv("RABBITMQ_GRADER_SERVICE_USERNAME")}:{os.getenv("RABBITMQ_GRADER_SERVICE_PASSWORD")}@rabbitmq-grader-service.{{.Release.Namespace}}.svc.cluster.local'

c.CeleryApp.conf = dict(
broker_url=broker_url,
result_backend='rpc://',
task_serializer='json',
result_serializer='json',
accept_content=['json'],
broker_connection_retry_on_startup=True,
)

c.LTISyncGrades.enable_lti_features = {{ .Values.ltiSyncGrades.enabled }}
c.LTISyncGrades.token_url = {{ .Values.ltiSyncGrades.token_url | quote }}
c.LTISyncGrades.client_id = {{ .Values.ltiSyncGrades.client_id | quote }}
Expand Down
11 changes: 11 additions & 0 deletions charts/grader-service/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ spec:
value: {{ .Values.db.host }}
- name: GRADER_DB_URL
value: {{ .Values.db.url }}
- name: RABBITMQ_GRADER_SERVICE_USERNAME
valueFrom:
secretKeyRef:
key: username
name: rabbitmq-grader-service-default-user
- name: RABBITMQ_GRADER_SERVICE_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: rabbitmq-grader-service-default-user
volumeMounts:
- name: data
mountPath: /var/lib/grader-service
Expand Down Expand Up @@ -101,6 +111,7 @@ spec:
- name: db-migration
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
command: ["/bin/sh","-c", "grader-service-migrate"]
imagePullPolicy: {{ .Values.image.pullPolicy }}
volumeMounts:
- name: data
mountPath: /var/lib/grader-service
Expand Down
12 changes: 12 additions & 0 deletions charts/grader-service/templates/rabbitmq.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: rabbitmq.com/v1beta1
kind: RabbitmqCluster
metadata:
name: rabbitmq-grader-service
labels:
{{- include "grader-service.labels" . | nindent 4 }}
namespace: {{ .Release.Namespace }}
spec:
replicas: 1
resources:
{{- toYaml .Values.rabbitmq.resources | nindent 4 }}

15 changes: 13 additions & 2 deletions charts/grader-service/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,6 @@ gitConfig:
volumePermissions:
enabled: true

concurrentGradingTasks: 5

autogradeExecutorClass: KubeAutogradeExecutor
kubeAutogradeExecutor:
image: ghcr.io/tu-wien-datalab/grader-service-labextension
Expand Down Expand Up @@ -104,6 +102,19 @@ ingress:
# hosts:
# - chart-example.local

rabbitmq:
resources:
requests:
cpu: 2
memory: 4Gi
limits:
cpu: 2
memory: 4Gi

workers:
replication: 1
resources: {}

resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
Expand Down
20 changes: 16 additions & 4 deletions examples/k8s/grader-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ jupyterhub:
apiUrl: http://proxy-public/hub/api
baseUrl: /

db:
dialect: postgresql
url: "postgresql://grader-service:[email protected]:5432"

#db:
# dialect: postgresql
# url: "postgresql://grader-service:[email protected]:5432"

gitConfig:
gitUser: "grader-service"
Expand All @@ -17,7 +18,8 @@ gitConfig:
volumePermissions:
enabled: true

concurrentGradingTasks: 1
extraConfig: |
c.CeleryApp.worker_kwargs=dict(loglevel="INFO", concurrency=1)
autogradeExecutorClass: KubeAutogradeExecutor
kubeAutogradeExecutor:
image: ghcr.io/tu-wien-datalab/grader-service-labextension
Expand All @@ -31,3 +33,13 @@ capacity: "5G"

ingress:
enabled: true

rabbitmq:
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 500m
memory: 1Gi

20 changes: 14 additions & 6 deletions examples/k8s/hub-config.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# hub relates to the hub pod, responsible for running JupyterHub, its configured
# Authenticator class KubeSpawner, and its configured Proxy class
# ConfigurableHTTPProxy. KubeSpawner creates the user pods, and
Expand All @@ -22,9 +21,18 @@ hub:
c.JupyterHub.tornado_settings = { 'headers': {'Content-Security-Policy': "frame-ancestors localhost 127.0.0.1 'self'"} }
c.JupyterHub.load_groups = {
"lect1:instructor": ["admin", "instructor"],
"lect1:tutor": ["tutor"],
"lect1:student": ["student"]
"lect1:instructor": {
'users': ['admin', 'instructor'],
'properties': {},
},
"lect1:tutor": {
'users': ['tutor'],
'properties': {},
},
"lect1:student":{
'users': ['student'],
'properties': {},
}
}
c.KubeSpawner.debug = True
Expand All @@ -48,8 +56,8 @@ singleuser:
type: none
defaultUrl: /lab
image:
name: ghcr.io/tu-wien-datalab/grader-service-labextension
tag: "main"
name: ghcr.io/tu-wien-datalab/grader-labextension
tag: "latest"
extraEnv:
GRADER_HOST_URL: http://grader-service:4010

Expand Down
2 changes: 1 addition & 1 deletion examples/k8s/install_grader.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#/usr/bin/env bash
#!/usr/bin/env bash

helm repo add grader-service https://tu-wien-datalab.github.io/Grader-Service
helm repo update
Expand Down
2 changes: 1 addition & 1 deletion examples/k8s/install_grader_dev.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#/usr/bin/env bash
#!/usr/bin/env bash

helm upgrade --cleanup-on-fail \
--install my-grader ../../charts/grader-service \
Expand Down
4 changes: 2 additions & 2 deletions examples/k8s/install_hub.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#/usr/bin/env bash
#!/usr/bin/env bash

helm repo add jupyterhub https://jupyterhub.github.io/helm-chart/
helm repo update
Expand All @@ -11,6 +11,6 @@ helm upgrade --cleanup-on-fail \
--install my-jupyterhub jupyterhub/jupyterhub \
--namespace jupyter \
--create-namespace \
--version=2.0.0 \
--version=3.2.1 \
--values hub-config.yaml

2 changes: 1 addition & 1 deletion examples/k8s/install_postgresql.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#/usr/bin/env bash
#!/usr/bin/env bash

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
Expand Down
Empty file.
48 changes: 48 additions & 0 deletions grader_service/autograding/celery/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from typing import Union
from tornado_sqlalchemy import SQLAlchemy
from traitlets import Dict
from traitlets.config import SingletonConfigurable, MultipleInstanceError
from celery import Celery, current_app


class CeleryApp(SingletonConfigurable):
conf = Dict(default_value=dict(
broker_url='amqp://localhost',
result_backend='rpc://',
task_serializer='json',
result_serializer='json',
accept_content=['json'],
broker_connection_retry_on_startup=True
), help="Configuration for Celery app.").tag(config=True)

worker_kwargs = Dict(default_value={}, help="Keyword arguments to pass to celery Worker instance.").tag(config=True)

app: Celery
_db: Union[SQLAlchemy, None] = None

def __init__(self, config_file: Union[str, None] = None, **kwargs):
super().__init__(**kwargs)
if not self.config:
self.config_file = config_file
if config_file is None:
raise ValueError("Neither config nor config_path were passed to CeleryApp!")

from grader_service.main import GraderService
service = GraderService()
# config might not be loaded if the celery app was not initialized by the service (e.g. in a worker)
if not service.config:
service.load_config_file(self.config_file)
service.set_config()
self.update_config(service.config)

from grader_service.autograding.celery.tasks import app
self.app = app # update module level celery app from tasks.py
self.app.conf.update(self.conf)

@property
def db(self) -> SQLAlchemy:
if self._db is None:
self.log.info('Instantiating database connection')
from grader_service.main import GraderService, db
self._db = db(GraderService(config=self.config).db_url)
return self._db
Loading

0 comments on commit 97513fd

Please sign in to comment.