Metadata-Version: 2.4
Name: racetrack_job_runner
Version: 1.18.0
Summary: Racetrack Job Runner
Author-email: ERST <noreply@erst.dk>
License: Apache License 2.0
Project-URL: Homepage, https://github.com/TheRacetrack/job-runner-python-lib
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: License :: OSI Approved :: Apache Software License
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: PyYAML>=6.0.1
Requires-Dist: prometheus-client>=0.17.1
Requires-Dist: fastapi>=0.110.1
Requires-Dist: uvicorn[standard]>=0.29.0
Requires-Dist: python-multipart>=0.0.9
Requires-Dist: a2wsgi>=1.10.4
Requires-Dist: httpx>=0.24.0
Requires-Dist: Jinja2>=3.1.3
Requires-Dist: memray>=1.14.0
Dynamic: license-file

# Racetrack Job Runner library

This is a Python library that takes the Racetrack job and allows to run it in an API server.
This runner (aka "wrapper") embeds given Python class in a REST server and adds extra features.

The library can be imported and run locally without using Racetrack services at all.
Nevertheless, it is compatible with Racetrack standards and to run it there,
you can pack your Job into a Dockerfile and deploy it to Racetrack, using
[generic "dockerfile" job type](https://github.com/TheRacetrack/plugin-dockerfile-job-type).
Moreover, the outcome Docker image is nothing special, but just a regular API server.
It can be tested locally and deployed anywhere else.

Check out [Changelog](./docs/CHANGELOG.md) to find out about notable changes.

## Installation
You can locally install this Job runner to your local environment by doing:
```sh
pip install racetrack-job-runner
```
or
```sh
pip install "git+https://github.com/TheRacetrack/job-runner-python-lib.git@master"
```

This will install `racetrack_job_runner` CLI executable
along with `racetrack_job_wrapper` module that can be imported later on.

## Example 1: Run Python class with CLI executable
Create a sample Job class `job.py` like this:  
```python
import math

class Job:
    def perform(self, number: int) -> bool:
        """Check if a number is prime"""
        if number < 2:
            return False
        for i in range(2, int(math.sqrt(number)) + 1):
            if number % i == 0:
                return False
        return True

    def docs_input_example(self) -> dict:
        """Return example input values for this model"""
        return {'number': 7907}
```

Now you can run it on localhost inside an API server
by calling this command in the terminal:
```sh
racetrack_job_runner run job.py
```

This will run your Job at [0.0.0.0:7000](http://0.0.0.0:7000). You can visit it in your browser.

This runner does a lot of things, including:

- Serve a Job at HTTP API server.
  It calls your Python `def perform` method whenever it receives a request to `/api/v1/perform`.
- Serve automatically generated SwaggerUI documentation
- Measure and serve Prometheus metrics at `/metrics` endpoint, reporting:
  - how many times the job was called
  - how long it took to call the job
  - how many errors have occurred
- Map Job's methods to auxiliary endpoints
- Serve static resources
- Serve Custom Webview UI (if defined in a Job's entrypoint)
- Serve empty API server, responding to liveness and readiness probes, while loading the Job
- Concurrent requests cap
- Make chain calls to other jobs
- Structured logging
- Keep record of a caller in the logs
- Show exemplary input payload in documentation (if defined in a Job's entrypoint)

## Example 2: Run from Python code
Alternatively, you can launch the job runner from your Python code.
See a [sample dockerfile job](./sample/dockerfiled) using this library in action.

Assuming you have a Job class [job.py](./sample/dockerfiled/job.py),
you can run it inside an API server (and provide a lot of extra features).
Call `racetrack_job_wrapper.standalone.serve_job_class` function in your Python program,
combined with your `Job` class:  
#### **`main.py`**
```python
from racetrack_job_wrapper.standalone import serve_job_class
from job import Job

def main():
    serve_job_class(Job)

if __name__ == '__main__':
    main()
```

Execute `python main.py` and this will run your Job at `0.0.0.0:7000` just as in the example 1.

## Running modes
There are several ways to run the same job.

### 1. Run on localhost with CLI executable
```shell
racetrack_job_runner run sample/dockerfiled/job.py
```

### 2. Run on localhost from Python
```shell
cd sample/dockerfiled
export JOB_NAME=primer JOB_VERSION=0.0.1
python main.py
```

### 3. Run in Docker
```shell
cd sample/dockerfiled &&\
docker buildx build -t sample-primer-job:latest -f Dockerfile .
docker run --rm -it --name sample-primer-job -p 7000:7000 sample-primer-job:latest
```

### 4. Run on Racetrack
```shell
racetrack deploy sample/dockerfiled
```

## Job configuration
You may tweak additional features of your job by specifying extra fields in a `job.yaml` manifest:

```yaml
jobtype_extra:
  max_concurrency: 1
  max_concurrency_queue: 10
  home_page: '/api/v1/webview'
```

Manifest YAML will be passed as environment variable to the Job by Racetrack's infrastructure plugin.
That's how this library can access this configuration.

### Concurrent requests cap
Maximum number of concurrent requests can be limited by `jobtype_extra.max_concurrency` field:
By default, concurrent requests are unlimited. Setting `max_concurrency` to `1` will make the job
process requests one by one. Overdue requests will be queued and processed in order.

Having such concurrency limits may cause some requests to wait in a queue.
If a throughput is higher than the job can handle, the queue will grow indefinitely.
To prevent that, you can set `jobtype_extra.max_concurrency_queue` to limit the queue size.
When the queue is full, the job will return `429 Too Many Requests` status code.

Example (1 request at a time, with up to 10 requests waiting in a queue):
```yaml
jobtype_extra:
  max_concurrency: 1
  max_concurrency_queue: 10
```

### Home page
You can configure the home page of your job.
Home page is the one you see when opening a job through the Dashboard or at the root endpoint.
By default, it shows the SwaggerUI page. Now you can change it, for instance, to a webview endpoint:
```yaml
jobtype_extra:
  home_page: '/api/v1/webview'
```

### Caller name
Setting environment variable `LOG_CALLER_NAME=true` allows you to keep record of a caller in the job's logs.
```dockerfile
ENV LOG_CALLER_NAME "true"
```
This will add caller identity (username or ESC name) to every log entry.

### Logging
To produce logs, use `logging` module inside your job:
```python
import logging
logger = logging.getLogger(__name__)

class JobEntrypoint:
    def perform(self):
        logger.info('something happened')
```

## Comparison with Python job type
This library has been originated from
[Python job type plugin](https://github.com/TheRacetrack/plugin-python-job-type).
If you need to recreate some of its features, take a look at the
[job Dockerfile template](https://github.com/TheRacetrack/plugin-python-job-type/blob/master/src/job-template.Dockerfile)
and do the same in your individual Job's Dockerfile.

In contrast to Python job type plugin, you directly call the utility functions in your code, you bind the things together.
This is opposed to a job type, where the framework calls your code.
This project transitions from a framework into a library approach, giving you more control.
