mirror of https://github.com/dapr/quickstarts.git
Merge pull request #1230 from kendallroden/python-jobs-sdk-quickstart
init jobs quickstart for python sdk
This commit is contained in:
commit
93becdd9a8
|
@ -4,7 +4,6 @@ In this quickstart, you'll schedule, get, and delete a job using Dapr's Job API.
|
|||
|
||||
Visit [this](https://docs.dapr.io/developing-applications/building-blocks/jobs/) link for more information about Dapr and the Jobs API.
|
||||
|
||||
|
||||
This quickstart includes two apps:
|
||||
|
||||
- `job-scheduler/app.py`, responsible for scheduling, retrieving and deleting jobs.
|
||||
|
@ -21,31 +20,42 @@ This quickstart includes two apps:
|
|||
- `JOB_SERVICE_DAPR_HTTP_PORT`: The Dapr HTTP port of the job-service (default: 6280)
|
||||
- `DAPR_HOST`: The Dapr host address (default: http://localhost)
|
||||
|
||||
## Install dependencies
|
||||
|
||||
<!-- STEP
|
||||
name: Install python dependencies
|
||||
-->
|
||||
```bash
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
<!-- END_STEP -->
|
||||
|
||||
## Run all apps with multi-app run template file
|
||||
|
||||
This section shows how to run both applications at once using [multi-app run template files](https://docs.dapr.io/developing-applications/local-development/multi-app-dapr-run/multi-app-overview/) with `dapr run -f .`. This enables you to test the interactions between multiple applications and will `schedule`, `run`, `get`, and `delete` jobs within a single process.
|
||||
|
||||
Open a new terminal window and run the multi app run template:
|
||||
1. Build the apps:
|
||||
|
||||
<!-- STEP
|
||||
name: Install python dependencies
|
||||
-->
|
||||
|
||||
```bash
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
<!-- END_STEP -->
|
||||
|
||||
2. Run the multi app run template:
|
||||
|
||||
<!-- STEP
|
||||
name: Run multi app run template
|
||||
expected_stdout_lines:
|
||||
- '== APP - job-scheduler == Sending request to schedule job: R2-D2'
|
||||
- '== APP - job-scheduler == Job scheduled: R2-D2'
|
||||
- '== APP - job-scheduler == Sending request to retrieve job: R2-D2'
|
||||
- '== APP - job-scheduler == Job details for R2-D2: {"name":"R2-D2", "dueTime":"15s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"@type":"type.googleapis.com/google.protobuf.StringValue", "value":"R2-D2:Oil Change"}}, "failurePolicy":{"constant":{"interval":"1s", "maxRetries":3}}}'
|
||||
- '== APP - job-scheduler == Sending request to schedule job: C-3PO'
|
||||
- '== APP - job-scheduler == Job scheduled: C-3PO'
|
||||
- '== APP - job-service == Received job request...'
|
||||
- '== APP - job-service == Starting droid: R2-D2'
|
||||
- '== APP - job-service == Executing maintenance job: Oil Change'
|
||||
- '== APP - job-scheduler == Job Scheduled: C-3PO'
|
||||
- '== APP - job-scheduler == Sending request to retrieve job: C-3PO'
|
||||
- '== APP - job-scheduler == Job details for C-3PO: {"name":"C-3PO", "dueTime":"20s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"@type":"type.googleapis.com/google.protobuf.StringValue", "value":"C-3PO:Limb Calibration"}}, "failurePolicy":{"constant":{"interval":"1s", "maxRetries":3}}}'
|
||||
- '== APP - job-service == Received job request...'
|
||||
- '== APP - job-service == Starting droid: C-3PO'
|
||||
- '== APP - job-service == Executing maintenance job: Limb Calibration'
|
||||
expected_stderr_lines:
|
||||
expected_stderr_lines: []
|
||||
output_match_mode: substring
|
||||
match_order: none
|
||||
background: true
|
||||
|
@ -60,33 +70,37 @@ dapr run -f .
|
|||
The terminal console output should look similar to this, where:
|
||||
|
||||
- The `R2-D2` job is being scheduled.
|
||||
- The `R2-D2` job is being executed after 2 seconds.
|
||||
- The `R2-D2` job is being retrieved.
|
||||
- The `C-3PO` job is being scheduled.
|
||||
- The `C-3PO` job is being retrieved.
|
||||
- The `R2-D2` job is being executed after 15 seconds.
|
||||
- The `C-3PO` job is being executed after 20 seconds.
|
||||
|
||||
```text
|
||||
== APP - job-scheduler == Job Scheduled: R2-D2
|
||||
== APP - job-scheduler == Sending request to schedule job: R2-D2
|
||||
== APP - job-scheduler == Job scheduled: R2-D2
|
||||
== APP - job-scheduler == Sending request to retrieve job: R2-D2
|
||||
== APP - job-scheduler == Job details for R2-D2: {"name":"R2-D2", "dueTime":"15s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"@type":"type.googleapis.com/google.protobuf.StringValue", "value":"R2-D2:Oil Change"}}, "failurePolicy":{"constant":{"interval":"1s", "maxRetries":3}}}
|
||||
== APP - job-scheduler == Sending request to schedule job: C-3PO
|
||||
== APP - job-scheduler == Job scheduled: C-3PO
|
||||
== APP - job-service == Received job request...
|
||||
== APP - job-service == Starting droid: R2-D2
|
||||
== APP - job-service == Executing maintenance job: Oil Change
|
||||
== APP - job-scheduler == Job Scheduled: C-3PO
|
||||
== APP - job-scheduler == Job details: {"name":"C-3PO", "dueTime":"30s", "data":{"@type":"ttype.googleapis.com/google.protobuf.StringValue", "expression":"C-3PO:Limb Calibration"}}
|
||||
```
|
||||
|
||||
After 30 seconds, the terminal output should present the `C-3PO` job being processed:
|
||||
|
||||
```text
|
||||
== APP - job-service == 127.0.0.1 - - "POST /job/R2-D2 HTTP/1.1" 200 -
|
||||
== APP - job-scheduler == Sending request to retrieve job: C-3PO
|
||||
== APP - job-scheduler == Job details for C-3PO: {"name":"C-3PO", "dueTime":"20s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"@type":"type.googleapis.com/google.protobuf.StringValue", "value":"C-3PO:Limb Calibration"}}, "failurePolicy":{"constant":{"interval":"1s", "maxRetries":3}}}
|
||||
== APP - job-service == Received job request...
|
||||
== APP - job-service == Starting droid: C-3PO
|
||||
== APP - job-service == Executing maintenance job: Limb Calibration
|
||||
== APP - job-service == 127.0.0.1 - - "POST /job/C-3PO HTTP/1.1" 200 -
|
||||
```
|
||||
|
||||
<!-- END_STEP -->
|
||||
|
||||
2. Stop and clean up application processes
|
||||
2. Stop and clean up application processes using a new terminal window.
|
||||
|
||||
<!-- STEP
|
||||
name: Stop multi-app run
|
||||
name: Stop multi-app run
|
||||
sleep: 5
|
||||
-->
|
||||
|
||||
|
@ -98,18 +112,20 @@ dapr stop -f .
|
|||
|
||||
## Run apps individually
|
||||
|
||||
### Start the job service
|
||||
### Schedule jobs
|
||||
|
||||
1. Open a terminal and run the `job-service` app:
|
||||
1. Open a terminal and run the `job-service` app. Build the dependencies if you haven't already.
|
||||
|
||||
```bash
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
```bash
|
||||
cd job-service
|
||||
dapr run --app-id job-service --app-port 6200 --dapr-http-port 6280 -- python app.py
|
||||
dapr run --app-id job-service --app-port 6200 --dapr-http-port 6280 --dapr-grpc-port 6281 -- python app.py
|
||||
```
|
||||
|
||||
### Schedule jobs
|
||||
|
||||
1. On a new terminal window, schedule the `R2-D2` Job using the Jobs API:
|
||||
2. In a new terminal window, schedule the `R2-D2` Job using the Jobs API:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
|
@ -124,7 +140,7 @@ curl -X POST \
|
|||
}'
|
||||
```
|
||||
|
||||
Back at the `job-service` app terminal window, the output should be:
|
||||
In the `job-service` app terminal window, the output should be:
|
||||
|
||||
```text
|
||||
== APP - job-service == Received job request...
|
||||
|
@ -132,7 +148,7 @@ Back at the `job-service` app terminal window, the output should be:
|
|||
== APP - job-service == Executing maintenance job: Oil Change
|
||||
```
|
||||
|
||||
2. On the same terminal window, schedule the `C-3PO` Job using the Jobs API:
|
||||
3. On the same terminal window, schedule the `C-3PO` Job using the Jobs API:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
|
@ -158,7 +174,7 @@ curl -X GET http://localhost:6280/v1.0-alpha1/jobs/c-3po -H "Content-Type: appli
|
|||
You should see the following:
|
||||
|
||||
```text
|
||||
{"name":"C-3PO", "dueTime":"30s", "data":{"@type":"type.googleapis.com/google.protobuf.StringValue", "expression":"C-3PO:Limb Calibration"}}
|
||||
{"name":"c-3po", "dueTime":"30s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"@type":"type.googleapis.com/google.protobuf.StringValue", "value":"C-3PO:Limb Calibration"}}, "failurePolicy":{"constant":{"interval":"1s", "maxRetries":3}}}
|
||||
```
|
||||
|
||||
### Delete a scheduled job
|
||||
|
@ -178,5 +194,5 @@ curl -X GET http://localhost:6280/v1.0-alpha1/jobs/c-3po -H "Content-Type: appli
|
|||
You should see an error message indicating that the job was not found:
|
||||
|
||||
```text
|
||||
{"errorCode":"ERR_JOBS_NOT_FOUND","message":"job not found: app||default||job-service||c-3po"}
|
||||
```
|
||||
{"errorCode":"DAPR_SCHEDULER_GET_JOB","message":"failed to get job due to: rpc error: code = NotFound desc = job not found: c-3po","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","domain":"dapr.io","metadata":{"appID":"job-service","namespace":"default"},"reason":"DAPR_SCHEDULER_GET_JOB"}]}
|
||||
```
|
||||
|
|
|
@ -5,17 +5,23 @@ import json
|
|||
|
||||
C3PO_JOB_BODY = {
|
||||
"data": {"@type": "type.googleapis.com/google.protobuf.StringValue", "value": "C-3PO:Limb Calibration"},
|
||||
"dueTime": "10s",
|
||||
"dueTime": "20s",
|
||||
}
|
||||
|
||||
R2D2_JOB_BODY = {
|
||||
"data": {"@type": "type.googleapis.com/google.protobuf.StringValue", "value": "R2-D2:Oil Change"},
|
||||
"dueTime": "2s"
|
||||
"dueTime": "15s"
|
||||
}
|
||||
|
||||
dapr_host = os.getenv('DAPR_HOST', 'http://localhost')
|
||||
job_service_dapr_http_port = os.getenv('JOB_SERVICE_DAPR_HTTP_PORT', '6280')
|
||||
|
||||
|
||||
def schedule_job(host: str, port: str, job_name: str, job_body: dict) -> None:
|
||||
req_url = f"{host}:{port}/v1.0-alpha1/jobs/{job_name}"
|
||||
|
||||
|
||||
print(f"Sending request to schedule job: {job_name}", flush=True)
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
req_url,
|
||||
|
@ -23,51 +29,57 @@ def schedule_job(host: str, port: str, job_name: str, job_body: dict) -> None:
|
|||
headers={"Content-Type": "application/json"},
|
||||
timeout=15
|
||||
)
|
||||
|
||||
# Accept both 200 and 204 as success codes
|
||||
if response.status_code not in [200, 204]:
|
||||
raise Exception(f"Failed to schedule job. Status code: {response.status_code}, Response: {response.text}")
|
||||
|
||||
print(f"Job Scheduled: {job_name}")
|
||||
raise Exception(
|
||||
f"Failed to schedule job. Status code: {response.status_code}, Response: {response.text}")
|
||||
|
||||
print(f"Job scheduled: {job_name}", flush=True)
|
||||
|
||||
if response.text:
|
||||
print(f"Response: {response.text}")
|
||||
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error scheduling job {job_name}: {str(e)}")
|
||||
print(f"Error scheduling job {job_name}: {str(e)}", flush=True)
|
||||
raise
|
||||
|
||||
|
||||
def get_job_details(host: str, port: str, job_name: str) -> None:
|
||||
req_url = f"{host}:{port}/v1.0-alpha1/jobs/{job_name}"
|
||||
|
||||
|
||||
print(f"Sending request to retrieve job: {job_name}", flush=True)
|
||||
|
||||
try:
|
||||
response = requests.get(req_url, timeout=15)
|
||||
if response.status_code in [200, 204]:
|
||||
print(f"Job details for {job_name}: {response.text}")
|
||||
print(f"Job details for {job_name}: {response.text}", flush=True)
|
||||
else:
|
||||
print(f"Failed to get job details. Status code: {response.status_code}, Response: {response.text}")
|
||||
|
||||
print(
|
||||
f"Failed to get job details. Status code: {response.status_code}, Response: {response.text}")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error getting job details for {job_name}: {str(e)}")
|
||||
print(
|
||||
f"Error getting job details for {job_name}: {str(e)}", flush=True)
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
# Wait for services to be ready
|
||||
time.sleep(5)
|
||||
|
||||
dapr_host = os.getenv('DAPR_HOST', 'http://localhost')
|
||||
job_service_dapr_http_port = os.getenv('JOB_SERVICE_DAPR_HTTP_PORT', '6280')
|
||||
|
||||
# Schedule R2-D2 job
|
||||
schedule_job(dapr_host, job_service_dapr_http_port, "R2-D2", R2D2_JOB_BODY)
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
# Get R2-D2 job details
|
||||
get_job_details(dapr_host, job_service_dapr_http_port, "R2-D2")
|
||||
time.sleep(5)
|
||||
# Schedule C-3PO job
|
||||
schedule_job(dapr_host, job_service_dapr_http_port, "C-3PO", C3PO_JOB_BODY)
|
||||
time.sleep(5)
|
||||
|
||||
# Get C-3PO job details
|
||||
get_job_details(dapr_host, job_service_dapr_http_port, "C-3PO")
|
||||
time.sleep(5)
|
||||
time.sleep(30)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
|
|
@ -25,7 +25,6 @@ class JobHandler(BaseHTTPRequestHandler):
|
|||
|
||||
def do_POST(self):
|
||||
print('Received job request...', flush=True)
|
||||
|
||||
try:
|
||||
# Check if path starts with /job/
|
||||
if not self.path.startswith('/job/'):
|
||||
|
@ -51,7 +50,7 @@ class JobHandler(BaseHTTPRequestHandler):
|
|||
self._send_response(200)
|
||||
|
||||
except Exception as e:
|
||||
print("Error processing job request:", flush= True)
|
||||
print("Error processing job request:", flush=True)
|
||||
print(traceback.format_exc())
|
||||
self._send_response(400, f"Error processing job: {str(e)}")
|
||||
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
# Dapr Jobs API (SDK)
|
||||
|
||||
In this quickstart, you'll schedule, get, and delete a job using Dapr's Job API. This API is responsible for scheduling and running jobs at a specific time or interval.
|
||||
|
||||
Visit [this](https://docs.dapr.io/developing-applications/building-blocks/jobs/) link for more information about Dapr and the Jobs API.
|
||||
|
||||
> **Note:** This example leverages the Python SDK. If you are looking for the example using only HTTP requests, [click here](../http/).
|
||||
|
||||
This quickstart includes two apps:
|
||||
|
||||
- `job-scheduler/app.py`, responsible for scheduling, retrieving and deleting jobs.
|
||||
- `job-service/app.py`, responsible for handling the triggered jobs.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Python 3.8+](https://www.python.org/downloads/)
|
||||
- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/)
|
||||
- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/)
|
||||
|
||||
## Run all apps with multi-app run template file
|
||||
|
||||
This section shows how to run both applications at once using [multi-app run template files](https://docs.dapr.io/developing-applications/local-development/multi-app-dapr-run/multi-app-overview/) with `dapr run -f .`. This enables you to test the interactions between multiple applications and will `schedule`, `run`, `get`, and `delete` jobs within a single process.
|
||||
|
||||
1. Build the apps:
|
||||
|
||||
<!-- STEP
|
||||
name: Install python dependencies
|
||||
-->
|
||||
|
||||
```bash
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
<!-- END_STEP -->
|
||||
|
||||
2. Run the multi app run template:
|
||||
|
||||
<!-- STEP
|
||||
name: Run multi app run template
|
||||
expected_stdout_lines:
|
||||
- '== APP - job-scheduler-sdk == Sending request to schedule job: R2-D2'
|
||||
- '== APP - job-service-sdk == Scheduling job: R2-D2'
|
||||
- '== APP - job-service-sdk == Job scheduled: R2-D2'
|
||||
- '== APP - job-scheduler-sdk == Response: {"name":"R2-D2","job":"Oil Change","dueTime":15}'
|
||||
- '== APP - job-scheduler-sdk == Sending request to retrieve job: R2-D2'
|
||||
- '== APP - job-service-sdk == Retrieving job: R2-D2'
|
||||
- '== APP - job-scheduler-sdk == Job details for R2-D2: {"name":"R2-D2","due_time":"15s","data":{"droid":"R2-D2","task":"Oil Change"}}'
|
||||
- '== APP - job-scheduler-sdk == Sending request to schedule job: C-3PO'
|
||||
- '== APP - job-service-sdk == Scheduling job: C-3PO'
|
||||
- '== APP - job-service-sdk == Job scheduled: C-3PO'
|
||||
- '== APP - job-service-sdk == Starting droid: R2-D2'
|
||||
- '== APP - job-service-sdk == Executing maintenance job: Oil Change'
|
||||
- '== APP - job-scheduler-sdk == Response: {"name":"C-3PO","job":"Limb Calibration","dueTime":20}'
|
||||
- '== APP - job-scheduler-sdk == Sending request to retrieve job: C-3PO'
|
||||
- '== APP - job-service-sdk == Retrieving job: C-3PO'
|
||||
- '== APP - job-scheduler-sdk == Job details for C-3PO: {"name":"C-3PO","due_time":"20s","data":{"droid":"C-3PO","task":"Limb Calibration"}}'
|
||||
expected_stderr_lines:
|
||||
output_match_mode: substring
|
||||
match_order: none
|
||||
background: true
|
||||
sleep: 60
|
||||
timeout_seconds: 120
|
||||
-->
|
||||
|
||||
```bash
|
||||
dapr run -f .
|
||||
```
|
||||
|
||||
The terminal console output should look similar to this, where:
|
||||
|
||||
- The `R2-D2` job is being scheduled.
|
||||
- The `R2-D2` job is being retrieved.
|
||||
- The `C-3PO` job is being scheduled.
|
||||
- The `C-3PO` job is being retrieved.
|
||||
- The `R2-D2` job is being executed after 15 seconds.
|
||||
- The `C-3PO` job is being executed after 20 seconds.
|
||||
|
||||
```text
|
||||
== APP - job-scheduler-sdk == Sending request to schedule job: R2-D2
|
||||
== APP - job-service-sdk == Scheduling job: R2-D2
|
||||
== APP - job-service-sdk == client.schedule_job_alpha1(job=job, overwrite=True)
|
||||
== APP - job-service-sdk == Job scheduled: R2-D2
|
||||
== APP - job-service-sdk == INFO: 192.168.1.106:0 - "POST /scheduleJob HTTP/1.1" 200 OK
|
||||
== APP - job-scheduler-sdk == Response: {"name":"R2-D2","job":"Oil Change","dueTime":15}
|
||||
== APP - job-scheduler-sdk == Sending request to retrieve job: R2-D2
|
||||
== APP - job-service-sdk == job = client.get_job_alpha1(name)
|
||||
== APP - job-service-sdk == Retrieving job: R2-D2
|
||||
== APP - job-service-sdk == INFO: 192.168.1.106:0 - "GET /getJob/R2-D2 HTTP/1.1" 200 OK
|
||||
== APP - job-scheduler-sdk == Job details for R2-D2: {"name":"R2-D2","due_time":"15s","data":{"droid":"R2-D2","task":"Oil Change"}}
|
||||
== APP - job-scheduler-sdk == Sending request to schedule job: C-3PO
|
||||
== APP - job-service-sdk == Scheduling job: C-3PO
|
||||
== APP - job-service-sdk == Job scheduled: C-3PO
|
||||
== APP - job-service-sdk == INFO: 192.168.1.106:0 - "POST /scheduleJob HTTP/1.1" 200 OK
|
||||
== APP - job-service-sdk == Starting droid: R2-D2
|
||||
== APP - job-service-sdk == Executing maintenance job: Oil Change
|
||||
== APP - job-service-sdk == INFO: 127.0.0.1:57206 - "POST /job/R2-D2 HTTP/1.1" 200 OK
|
||||
== APP - job-scheduler-sdk == Response: {"name":"C-3PO","job":"Limb Calibration","dueTime":20}
|
||||
== APP - job-scheduler-sdk == Sending request to retrieve job: C-3PO
|
||||
== APP - job-service-sdk == Retrieving job: C-3PO
|
||||
== APP - job-service-sdk == INFO: 192.168.1.106:0 - "GET /getJob/C-3PO HTTP/1.1" 200 OK
|
||||
== APP - job-scheduler-sdk == Job details for C-3PO: {"name":"C-3PO","due_time":"20s","data":{"droid":"C-3PO","task":"Limb Calibration"}}
|
||||
```
|
||||
|
||||
<!-- END_STEP -->
|
||||
|
||||
2. Stop and clean up application processes using a new terminal window
|
||||
|
||||
<!-- STEP
|
||||
name: Stop multi-app run
|
||||
sleep: 5
|
||||
-->
|
||||
|
||||
```bash
|
||||
dapr stop -f .
|
||||
```
|
||||
|
||||
<!-- END_STEP -->
|
||||
|
||||
## Run apps individually
|
||||
|
||||
### Schedule jobs
|
||||
|
||||
1. Open a terminal and run the `job-service-sdk` app. Build the dependencies if you haven't already.
|
||||
|
||||
```bash
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
```bash
|
||||
cd job-service-sdk
|
||||
dapr run --app-id job-service-sdk --app-port 6200 --dapr-http-port 6280 --dapr-grpc-port 6281 -- python app.py
|
||||
```
|
||||
|
||||
2. In a new terminal window, schedule the `R2-D2` Job using the Jobs API:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
http://localhost:6200/scheduleJob \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "R2-D2",
|
||||
"job": "Oil Change",
|
||||
"dueTime": 2
|
||||
}'
|
||||
```
|
||||
|
||||
In the `job-service-sdk` terminal window, the output should be:
|
||||
|
||||
```text
|
||||
|
||||
== APP == Scheduling job: R2-D2
|
||||
== APP == Job scheduled: R2-D2
|
||||
== APP == INFO: 127.0.0.1:59756 - "POST /scheduleJob HTTP/1.1" 200 OK
|
||||
== APP == Starting droid: R2-D2
|
||||
== APP == Executing maintenance job: Oil Change
|
||||
== APP == INFO: 127.0.0.1:59759 - "POST /job/R2-D2 HTTP/1.1" 200 OK
|
||||
```
|
||||
|
||||
3. On the same terminal window, schedule the `C-3PO` Job using the Jobs API:
|
||||
|
||||
```bash
|
||||
curl -X POST \
|
||||
http://localhost:6200/scheduleJob \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "C-3PO",
|
||||
"job": "Limb Calibration",
|
||||
"dueTime": 30
|
||||
}'
|
||||
```
|
||||
|
||||
### Get a scheduled job
|
||||
|
||||
1. On the same terminal window, run the command below to get the recently scheduled `C-3PO` job:
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:6200/getJob/C-3PO -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
You should see the following:
|
||||
|
||||
```text
|
||||
{"name":"C-3PO","due_time":"30s","data":{"droid":"C-3PO","task":"Limb Calibration"}}
|
||||
```
|
||||
|
||||
### Delete a scheduled job
|
||||
|
||||
1. On the same terminal window, run the command below to delete the recently scheduled `C-3PO` job:
|
||||
|
||||
```bash
|
||||
curl -X DELETE http://localhost:6200/deleteJob/C-3PO -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
You should see the following:
|
||||
|
||||
```text
|
||||
{"message":"Job deleted"}
|
||||
```
|
||||
|
||||
2. Run the command below to attempt to retrieve the deleted job:
|
||||
|
||||
```bash
|
||||
curl -X GET http://localhost:6200/getJob/C-3PO -H "Content-Type: application/json"
|
||||
```
|
||||
|
||||
In the `job-service-sdk` terminal window, the output should be similar to the following:
|
||||
|
||||
```text
|
||||
{"detail":"<_InactiveRpcError of RPC that terminated with:\n\tstatus = StatusCode.INTERNAL\n\tdetails = \"failed to get job due to: rpc error: code = NotFound desc = job not found: C-3PO\"\n\tdebug_error_string = \"UNKNOWN:Error received from peer ipv4:127.0.0.1:6281 {grpc_status:13, grpc_message:\"failed to get job due to: rpc error: code = NotFound desc = job not found: C-3PO\"}\"\n>"}
|
||||
```
|
|
@ -0,0 +1,18 @@
|
|||
version: 1
|
||||
apps:
|
||||
- appDirPath: ./job-service/
|
||||
appID: job-service-sdk
|
||||
appPort: 6200
|
||||
daprHTTPPort: 6280
|
||||
daprGRPCPort: 6281
|
||||
appLogDestination: console
|
||||
daprdLogDestination: console
|
||||
command: ["python3", "app.py"]
|
||||
- appDirPath: ./job-scheduler/
|
||||
appID: job-scheduler-sdk
|
||||
appPort: 6300
|
||||
daprHTTPPort: 6380
|
||||
daprGRPCPort: 6381
|
||||
appLogDestination: console
|
||||
daprdLogDestination: console
|
||||
command: ["python3", "app.py"]
|
|
@ -0,0 +1,108 @@
|
|||
import os
|
||||
import json
|
||||
import time
|
||||
from typing import Optional
|
||||
from dataclasses import dataclass
|
||||
import requests
|
||||
|
||||
|
||||
@dataclass
|
||||
class DroidJob:
|
||||
name: Optional[str] = None
|
||||
job: Optional[str] = None
|
||||
due_time: int = 1
|
||||
|
||||
|
||||
# Job details
|
||||
r2d2_job = DroidJob(name="R2-D2", job="Oil Change", due_time=15)
|
||||
c3po_job = DroidJob(name="C-3PO", job="Limb Calibration", due_time=20)
|
||||
|
||||
dapr_host = os.getenv('DAPR_HOST', 'http://localhost')
|
||||
dapr_port = os.getenv('DAPR_HTTP_PORT', '3500')
|
||||
|
||||
|
||||
def schedule_job(job: DroidJob) -> None:
|
||||
|
||||
print(f"Sending request to schedule job: {job.name}", flush=True)
|
||||
|
||||
try:
|
||||
# Convert the job to a dictionary for JSON serialization
|
||||
job_data = {
|
||||
"name": job.name,
|
||||
"job": job.job,
|
||||
"dueTime": job.due_time
|
||||
}
|
||||
|
||||
# Use HTTP client to call the job-service-sdk via Dapr
|
||||
req_url = f"{dapr_host}:{dapr_port}/v1.0/invoke/job-service-sdk/method/scheduleJob"
|
||||
|
||||
response = requests.post(
|
||||
req_url,
|
||||
json=job_data,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=15
|
||||
)
|
||||
|
||||
# Accept both 200 and 204 as success codes
|
||||
if response.status_code not in [200, 204]:
|
||||
raise Exception(
|
||||
f"Failed to schedule job. Status code: {response.status_code}, Response: {response.text}", flush=True)
|
||||
|
||||
if response.text:
|
||||
print(f"Response: {response.text}")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error scheduling job {job.name}: {str(e)}", flush=True)
|
||||
raise
|
||||
|
||||
|
||||
def get_job_details(job: DroidJob) -> None:
|
||||
|
||||
print(f"Sending request to retrieve job: {job.name}", flush=True)
|
||||
|
||||
try:
|
||||
# Use HTTP client to call the job-service-sdk via Dapr
|
||||
req_url = f"{dapr_host}:{dapr_port}/v1.0/invoke/job-service-sdk/method/getJob/{job.name}"
|
||||
|
||||
response = requests.get(req_url)
|
||||
|
||||
if response.status_code in [200, 204]:
|
||||
print(f"Job details for {job.name}: {response.text}", flush=True)
|
||||
else:
|
||||
print(
|
||||
f"Failed to get job details. Status code: {response.status_code}, Response: {response.text}")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(
|
||||
f"Error getting job details for {job.name}: {str(e)}", flush=True)
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
# Allow time for the job-service-sdk to start
|
||||
time.sleep(5)
|
||||
|
||||
try:
|
||||
# Schedule R2-D2 job
|
||||
schedule_job(r2d2_job)
|
||||
time.sleep(5)
|
||||
|
||||
# Get R2-D2 job details
|
||||
get_job_details(r2d2_job)
|
||||
time.sleep(5)
|
||||
|
||||
# Schedule C-3PO job
|
||||
schedule_job(c3po_job)
|
||||
time.sleep(5)
|
||||
|
||||
# Get C-3PO job details
|
||||
get_job_details(c3po_job)
|
||||
time.sleep(30) # Allow time for jobs to complete
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -0,0 +1,188 @@
|
|||
import os
|
||||
import json
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
from fastapi import FastAPI, HTTPException, Response
|
||||
from pydantic import BaseModel
|
||||
from dapr.clients import DaprClient, Job, DropFailurePolicy, ConstantFailurePolicy
|
||||
|
||||
# Add protobuf availability check
|
||||
try:
|
||||
from google.protobuf.any_pb2 import Any as GrpcAny
|
||||
PROTOBUF_AVAILABLE = True
|
||||
except ImportError:
|
||||
PROTOBUF_AVAILABLE = False
|
||||
print('Warning: protobuf not available, jobs with data will be scheduled without data', flush=True)
|
||||
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Initialize FastAPI app
|
||||
app = FastAPI(title="Dapr Jobs Service", version="1.0.0")
|
||||
|
||||
# Get app port from environment
|
||||
app_port = int(os.getenv('APP_PORT', '6200'))
|
||||
|
||||
# Pydantic models for request/response
|
||||
|
||||
|
||||
class JobData(BaseModel):
|
||||
droid: Optional[str] = None
|
||||
task: Optional[str] = None
|
||||
|
||||
|
||||
class DroidJob(BaseModel):
|
||||
name: Optional[str] = None
|
||||
job: Optional[str] = None
|
||||
dueTime: int
|
||||
|
||||
|
||||
def create_job_data(data_dict):
|
||||
# Create job data from a dictionary
|
||||
if not PROTOBUF_AVAILABLE:
|
||||
return None
|
||||
|
||||
data = GrpcAny()
|
||||
data.value = json.dumps(data_dict).encode('utf-8')
|
||||
return data
|
||||
|
||||
|
||||
@app.post("/scheduleJob")
|
||||
def schedule_job(droid_job: DroidJob, response: Response):
|
||||
|
||||
print(f"Scheduling job: {droid_job.name}", flush=True)
|
||||
|
||||
if not droid_job.name or not droid_job.job:
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Job must contain a name and a task")
|
||||
|
||||
try:
|
||||
# Create job data payload
|
||||
job_data = JobData(
|
||||
droid=droid_job.name,
|
||||
task=droid_job.job
|
||||
)
|
||||
|
||||
# Create the job
|
||||
job = Job(
|
||||
name=droid_job.name,
|
||||
due_time=f"{droid_job.dueTime}s",
|
||||
data=create_job_data(job_data.model_dump())
|
||||
)
|
||||
with DaprClient() as d:
|
||||
# Schedule the job
|
||||
d.schedule_job_alpha1(job=job, overwrite=True)
|
||||
|
||||
print(f"Job scheduled: {droid_job.name}", flush=True)
|
||||
|
||||
# Set 200 status and return the payload
|
||||
response.status_code = 200
|
||||
return droid_job
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error scheduling job: {e}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error scheduling job: {str(e)}")
|
||||
|
||||
|
||||
@app.get("/getJob/{name}")
|
||||
async def get_job(name: str):
|
||||
|
||||
print(f"Retrieving job: {name}")
|
||||
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="Job name required")
|
||||
|
||||
try:
|
||||
with DaprClient() as d:
|
||||
job = d.get_job_alpha1(name)
|
||||
|
||||
if job is None:
|
||||
raise HTTPException(status_code=404, detail="Job not found")
|
||||
|
||||
# Convert protobuf job object to dict for JSON serialization
|
||||
job_dict = {
|
||||
"name": job.name,
|
||||
"due_time": job.due_time,
|
||||
}
|
||||
|
||||
# Handle job data if present
|
||||
if job.data:
|
||||
try:
|
||||
payload = json.loads(job.data.value.decode('utf-8'))
|
||||
job_dict["data"] = payload
|
||||
except Exception:
|
||||
job_dict["data"] = f"<binary data, {len(job.data.value)} bytes>"
|
||||
else:
|
||||
job_dict["data"] = None
|
||||
|
||||
return job_dict
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error getting job: {e}")
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@app.delete("/deleteJob/{name}")
|
||||
async def delete_job(name: str):
|
||||
print(f"Deleting job: {name}")
|
||||
|
||||
if not name:
|
||||
raise HTTPException(status_code=400, detail="Job name required")
|
||||
|
||||
try:
|
||||
with DaprClient() as d:
|
||||
job_details = d.delete_job_alpha1(name)
|
||||
print(f"Job deleted: {name}")
|
||||
return {"message": "Job deleted"}
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error deleting job: {e}")
|
||||
raise HTTPException(status_code=400, detail="Error deleting job")
|
||||
|
||||
|
||||
@app.post("/job/{job_name}")
|
||||
async def handle_job(job_name: str, job_payload: dict):
|
||||
|
||||
try:
|
||||
# Extract job data from payload
|
||||
# The payload structure depends on how the job was scheduled
|
||||
if "droid" in job_payload and "task" in job_payload:
|
||||
droid = job_payload["droid"]
|
||||
task = job_payload["task"]
|
||||
else:
|
||||
# Fallback: try to extract from the raw payload
|
||||
payload_str = str(job_payload)
|
||||
print(f"Raw payload: {payload_str}")
|
||||
|
||||
# Try to parse as JSON if it's a string
|
||||
if isinstance(payload_str, str):
|
||||
try:
|
||||
parsed = json.loads(payload_str)
|
||||
droid = parsed.get("droid", "Unknown")
|
||||
task = parsed.get("task", "Unknown")
|
||||
except json.JSONDecodeError:
|
||||
droid = "Unknown"
|
||||
task = payload_str
|
||||
else:
|
||||
droid = "Unknown"
|
||||
task = str(job_payload)
|
||||
|
||||
# Execute the job
|
||||
print(f"Starting droid: {droid}", flush=True)
|
||||
print(f"Executing maintenance job: {task}", flush=True)
|
||||
|
||||
return {"status": "success", "droid": droid, "task": task}
|
||||
|
||||
except Exception as ex:
|
||||
print(f"Failed to handle job {job_name}")
|
||||
print(f"Error handling job: {ex}")
|
||||
raise HTTPException(
|
||||
status_code=500, detail=f"Error handling job: {str(ex)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=app_port)
|
|
@ -0,0 +1,2 @@
|
|||
include ../../../docker.mk
|
||||
include ../../../validate.mk
|
|
@ -0,0 +1,5 @@
|
|||
requests==2.31.0
|
||||
dapr==1.16.0rc1
|
||||
fastapi==0.104.1
|
||||
uvicorn==0.24.0
|
||||
pydantic==2.5.0
|
Loading…
Reference in New Issue