Skip to content

Commit

Permalink
feat: server status endpoint (#78)
Browse files Browse the repository at this point in the history
- updated qppe api and routes to include the new status model
- extended config.example.ini and settings.py with the allow_lms_packages option
  • Loading branch information
alexanderschmitz authored Dec 18, 2023
1 parent ebf9d18 commit 46fdc8f
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 3 deletions.
3 changes: 3 additions & 0 deletions config.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
# Maximum package size (at least 20 MiB)
#max_package_size = 20 MiB

# Allow packages from the LMS (default: True)
#allow_lms_packages = False

[worker]
# Fully qualified name of the worker class
#type = questionpy_server.worker.worker.subprocess.SubprocessWorker
Expand Down
46 changes: 46 additions & 0 deletions docs/qppe.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,18 @@ paths:
schema:
$ref: "#/components/schemas/RequestError"

/status:
parameters:
- $ref: '#/components/parameters/UserAgent'
get:
summary: Get information about the server status
responses:
200:
description: OK
content:
application/json:
schema:
$ref: "#/components/schemas/ServerStatus"

components:
parameters:
Expand Down Expand Up @@ -1334,3 +1346,37 @@ components:
items:
$ref: "#/components/schemas/FormElement"
required: [ name, header, elements ]

ServerStatus:
type: object
properties:
name:
type: string
example: questionpy-server
version:
type: string
format: semver
# https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
pattern: '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$'
example: 0.1.0
allow_lms_packages:
type: boolean
example: true
max_package_size:
type: integer
description: Maximum package size (in bytes)
usage:
$ref: "#/components/schemas/Usage"
required: [name, version, allow_lms_packages, max_package_size]

Usage:
type: object
description: Current usage of request handlers
properties:
requests_in_process:
type: integer
description: Amount of requests being processed
requests_in_queue:
type: integer
description: Amount of requests waiting to be processed
required: [requests_in_process, requests_in_queue]
2 changes: 1 addition & 1 deletion questionpy_server/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = '0.1'
__version__ = '0.1.0'

# This file is part of the QuestionPy Server. (https://questionpy.org)
# The QuestionPy Server is free software released under terms of the MIT license. See LICENSE.md.
Expand Down
18 changes: 17 additions & 1 deletion questionpy_server/api/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from enum import Enum
from typing import Annotated, Any, Dict, List, Optional, Union

from pydantic import ConfigDict, BaseModel, Field, FilePath, HttpUrl, Json
from pydantic import ConfigDict, BaseModel, Field, FilePath, HttpUrl, Json, ByteSize
from questionpy_common.elements import OptionsFormDefinition
from questionpy_common.models import AttemptModel, QuestionModel

Expand Down Expand Up @@ -139,3 +139,19 @@ class QuestionStateMigrationError(BaseModel):

code: QuestionStateMigrationErrorCode
reason: Optional[str] = None


class Usage(BaseModel):
requests_in_process: int
requests_in_queue: int


class ServerStatus(BaseModel):
name: str = 'questionpy-server'
version: Annotated[str, Field(pattern=r'^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)'
r'(-((0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)'
r'(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?'
r'(\+([0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*))?$')]
allow_lms_packages: bool
max_package_size: ByteSize
usage: Optional[Usage]
18 changes: 17 additions & 1 deletion questionpy_server/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@

from questionpy_server.factories import AttemptScoredFactory
from questionpy_server.web import ensure_package_and_question_state_exist, json_response
from questionpy_server import __version__
from .models import AttemptStartArguments, AttemptScoreArguments, AttemptViewArguments, \
QuestionCreateArguments, QuestionEditFormResponse, RequestBaseData
QuestionCreateArguments, QuestionEditFormResponse, RequestBaseData, ServerStatus, Usage
from ..package import Package
from ..worker.worker import Worker

Expand Down Expand Up @@ -122,3 +123,18 @@ async def post_question_migrate(_request: web.Request) -> web.Response:
async def package_extract_info(_request: web.Request, package: Package) -> web.Response:
"""Get package information."""
return json_response(data=package.get_info(), status=201)


@routes.get(r'/status')
async def get_server_status(request: web.Request) -> web.Response:
"""Get server status"""
qpyserver: 'QPyServer' = request.app['qpy_server_app']
status = ServerStatus(
version=__version__,
allow_lms_packages=qpyserver.settings.webservice.allow_lms_packages,
max_package_size=qpyserver.settings.webservice.max_package_size,
usage=Usage(
requests_in_process=await qpyserver.worker_pool.get_requests_in_process(),
requests_in_queue=await qpyserver.worker_pool.get_requests_in_queue()
))
return json_response(data=status, status=200)
1 change: 1 addition & 0 deletions questionpy_server/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ def logging_level_to_upper(cls, value: str) -> str:
class WebserviceSettings(BaseModel):
listen_address: str = '127.0.0.1'
listen_port: int = 9020
allow_lms_packages: bool = True

# Not configurable. Only here because it is analogous to max_package_size.
max_main_size: ClassVar[ByteSize] = ByteSize(5 * MiB)
Expand Down
26 changes: 26 additions & 0 deletions questionpy_server/worker/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ def __init__(self, max_workers: int, max_memory: int,
self._semaphore: Optional[Semaphore] = None
self._condition: Optional[Condition] = None

self._running_workers: int = 0
self._requests: int = 0

self._total_memory = 0

def memory_available(self, size: int) -> bool:
Expand All @@ -59,8 +62,12 @@ async def get_worker(self, package: Path, _lms: int, _context: Optional[int]) ->
if not self._condition:
self._condition = Condition()

self._requests += 1

# Limit the amount of running workers.
async with self._semaphore:
self._running_workers += 1

worker = None
reserved_memory = False
try:
Expand Down Expand Up @@ -89,3 +96,22 @@ async def get_worker(self, package: Path, _lms: int, _context: Optional[int]) ->
self._total_memory -= limits.max_memory
async with self._condition:
self._condition.notify_all()

self._running_workers -= 1
self._requests -= 1

async def get_requests_in_process(self) -> int:
"""Get the number of workers currently running.
Returns:
int: The count of workers currently running.
"""
return self._running_workers

async def get_requests_in_queue(self) -> int:
"""Get the number of pending requests.
Returns:
int: The count of pending requests.
"""
return self._requests - self._running_workers

0 comments on commit 46fdc8f

Please sign in to comment.