mirror of https://github.com/dapr/quickstarts.git
HTTP csharp jobs quickstart
Signed-off-by: Alice Gibbons <alice@diagrid.io>
This commit is contained in:
parent
b8ef1c01cf
commit
dcf1f9fb80
|
|
@ -0,0 +1,181 @@
|
||||||
|
# Dapr Jobs API (HTTP Client)
|
||||||
|
|
||||||
|
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 HTTP requests only. If you are looking for the example using the Dapr Client SDK (recommended) [click here](../sdk/).
|
||||||
|
|
||||||
|
This quickstart includes two apps:
|
||||||
|
|
||||||
|
- Jobs Scheduler, responsible for scheduling, retrieving and deleting jobs.
|
||||||
|
- Jobs Service, responsible for handling the triggered jobs.
|
||||||
|
|
||||||
|
## 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 to you test the interactions between multiple applications and will `schedule`, `run`, `get`, and `delete` jobs within a single process.
|
||||||
|
|
||||||
|
1. Build the apps:
|
||||||
|
|
||||||
|
<!-- STEP
|
||||||
|
name: Build dependencies for job-service
|
||||||
|
sleep: 1
|
||||||
|
-->
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ./job-service
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- END_STEP -->
|
||||||
|
|
||||||
|
<!-- STEP
|
||||||
|
name: Build dependencies for job-scheduler
|
||||||
|
sleep: 1
|
||||||
|
-->
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ./job-scheduler
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- END_STEP -->
|
||||||
|
|
||||||
|
2. Run the multi app run template:
|
||||||
|
|
||||||
|
<!-- STEP
|
||||||
|
name: Run multi app run template
|
||||||
|
expected_stdout_lines:
|
||||||
|
- '== APP - job-scheduler == Job Scheduled: R2-D2'
|
||||||
|
- '== 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-service == Received job request...'
|
||||||
|
- '== APP - job-service == Starting droid: C-3PO'
|
||||||
|
- '== APP - job-service == Executing maintenance job: Limb Calibration'
|
||||||
|
expected_stderr_lines:
|
||||||
|
output_match_mode: substring
|
||||||
|
match_order: none
|
||||||
|
background: false
|
||||||
|
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 == Job Scheduled: R2-D2
|
||||||
|
== APP - job-scheduler == Job details: {"name":"R2-D2", "dueTime":"15s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"Value":"R2-D2:Oil Change"}}}
|
||||||
|
== APP - job-scheduler == Job Scheduled: C-3PO
|
||||||
|
== APP - job-scheduler == Job details: {"name":"C-3PO", "dueTime":"20s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"Value":"C-3PO:Limb Calibration"}}}
|
||||||
|
== APP - job-service == Received job request...
|
||||||
|
== APP - job-service == Starting droid: R2-D2
|
||||||
|
== APP - job-service == Executing maintenance job: Oil Change
|
||||||
|
```
|
||||||
|
|
||||||
|
After 20 seconds, the terminal output should present the `C-3PO` job being processed:
|
||||||
|
|
||||||
|
```text
|
||||||
|
== APP - job-service == Received job request...
|
||||||
|
== APP - job-service == Starting droid: C-3PO
|
||||||
|
== APP - job-service == Executing maintenance job: Limb Calibration
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- END_STEP -->
|
||||||
|
|
||||||
|
## Run apps individually
|
||||||
|
|
||||||
|
### Schedule Jobs
|
||||||
|
|
||||||
|
1. Open a terminal and run the `job-service` app. Build the dependencies if you haven't already.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ./job-service
|
||||||
|
dotnet build
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
dapr run --app-id job-service --app-port 6200 --dapr-http-port 6280 -- dotnet run
|
||||||
|
```
|
||||||
|
|
||||||
|
2. In a new terminal window, schedule the `R2-D2` Job using the Jobs API.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
http://localhost:6280/v1.0-alpha1/jobs/r2-d2 \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"data": {
|
||||||
|
"Value": "R2-D2:Oil Change"
|
||||||
|
},
|
||||||
|
"dueTime": "2s"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
In the `job-service` terminal window, the output should be:
|
||||||
|
|
||||||
|
```text
|
||||||
|
== APP - job-app == Received job request...
|
||||||
|
== APP - job-app == Starting droid: R2-D2
|
||||||
|
== APP - job-app == Executing maintenance job: Oil Change
|
||||||
|
```
|
||||||
|
|
||||||
|
3. On the same terminal window, schedule the `C-3PO` Job using the Jobs API.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST \
|
||||||
|
http://localhost:6280/v1.0-alpha1/jobs/c-3po \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"data": {
|
||||||
|
"Value": "C-3PO:Limb Calibration"
|
||||||
|
},
|
||||||
|
"dueTime": "30s"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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:6280/v1.0-alpha1/jobs/c-3po -H "Content-Type: application/json"
|
||||||
|
```
|
||||||
|
|
||||||
|
You should see the following:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{"name":"c-3po", "dueTime":"30s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"Value":"C-3PO:Limb Calibration"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Delete a scheduled job
|
||||||
|
|
||||||
|
1. On the same terminal window, run the command below to deleted the recently scheduled `C-3PO` job.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X DELETE http://localhost:6280/v1.0-alpha1/jobs/c-3po -H "Content-Type: application/json"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run the command below to attempt to retrieve the deleted job:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET http://localhost:6280/v1.0-alpha1/jobs/c-3po -H "Content-Type: application/json"
|
||||||
|
```
|
||||||
|
|
||||||
|
In the `job-service` terminal window, the output should be similar to the following:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ERRO[0568] Error getting job c-3po due to: rpc error: code = Unknown desc = job not found: c-3po instance=local scope=dapr.api type=log ver=1.15.0
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
version: 1
|
||||||
|
apps:
|
||||||
|
- appDirPath: ./job-service/
|
||||||
|
appID: job-service
|
||||||
|
appPort: 6200
|
||||||
|
daprHTTPPort: 6280
|
||||||
|
schedulerHostAddress: localhost
|
||||||
|
command: ["dotnet", "run"]
|
||||||
|
- appDirPath: ./job-scheduler/
|
||||||
|
appID: job-scheduler
|
||||||
|
appPort: 6300
|
||||||
|
daprHTTPPort: 6380
|
||||||
|
command: ["dotnet", "run"]
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
// Job request bodies
|
||||||
|
var c3poJobBody = new
|
||||||
|
{
|
||||||
|
data = new { Value = "C-3PO:Limb Calibration" },
|
||||||
|
dueTime = "20s"
|
||||||
|
};
|
||||||
|
|
||||||
|
var r2d2JobBody = new
|
||||||
|
{
|
||||||
|
data = new { Value = "R2-D2:Oil Change" },
|
||||||
|
dueTime = "15s"
|
||||||
|
};
|
||||||
|
|
||||||
|
var daprHost = Environment.GetEnvironmentVariable("DAPR_HOST") ?? "http://localhost";
|
||||||
|
var schedulerDaprHttpPort = "6280";
|
||||||
|
|
||||||
|
var httpClient = new HttpClient();
|
||||||
|
|
||||||
|
await Task.Delay(5000); // Wait for job-service to start
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Schedule R2-D2 job
|
||||||
|
await ScheduleJob("R2-D2", r2d2JobBody);
|
||||||
|
await Task.Delay(5000);
|
||||||
|
// Get R2-D2 job details
|
||||||
|
await GetJobDetails("R2-D2");
|
||||||
|
|
||||||
|
// Schedule C-3PO job
|
||||||
|
await ScheduleJob("C-3PO", c3poJobBody);
|
||||||
|
await Task.Delay(5000);
|
||||||
|
// Get C-3PO job details
|
||||||
|
await GetJobDetails("C-3PO");
|
||||||
|
|
||||||
|
await Task.Delay(30000); // Allow time for jobs to complete
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error: {ex.Message}");
|
||||||
|
Environment.Exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task ScheduleJob(string jobName, object jobBody)
|
||||||
|
{
|
||||||
|
var reqURL = $"{daprHost}:{schedulerDaprHttpPort}/v1.0-alpha1/jobs/{jobName}";
|
||||||
|
var jsonBody = JsonSerializer.Serialize(jobBody);
|
||||||
|
var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await httpClient.PostAsync(reqURL, content);
|
||||||
|
|
||||||
|
if (response.StatusCode != System.Net.HttpStatusCode.NoContent)
|
||||||
|
{
|
||||||
|
throw new Exception($"Failed to register job event handler. Status code: {response.StatusCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Job Scheduled: {jobName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task GetJobDetails(string jobName)
|
||||||
|
{
|
||||||
|
var reqURL = $"{daprHost}:{schedulerDaprHttpPort}/v1.0-alpha1/jobs/{jobName}";
|
||||||
|
|
||||||
|
var response = await httpClient.GetAsync(reqURL);
|
||||||
|
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
throw new Exception($"HTTP error! Status: {response.StatusCode}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var jobDetails = await response.Content.ReadAsStringAsync();
|
||||||
|
Console.WriteLine($"Job details: {jobDetails}");
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<RootNamespace>jobs_scheduler</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http.Json;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.Configure<JsonOptions>(options =>
|
||||||
|
{
|
||||||
|
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
|
||||||
|
});
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
var appPort = Environment.GetEnvironmentVariable("APP_PORT") ?? "6200";
|
||||||
|
|
||||||
|
//Job handler route
|
||||||
|
app.MapPost("/job/{*path}", async (HttpRequest request, HttpResponse response) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine("Received job request...");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Parse the incoming JSON body
|
||||||
|
var jobData = await JsonSerializer.DeserializeAsync<JobData>(request.Body);
|
||||||
|
if (jobData == null || string.IsNullOrEmpty(jobData.Value))
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid job data. 'value' field is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creating Droid Job from decoded value
|
||||||
|
var droidJob = SetDroidJob(jobData.Value);
|
||||||
|
Console.WriteLine($"Starting droid: {droidJob.Droid}");
|
||||||
|
Console.WriteLine($"Executing maintenance job: {droidJob.Task}");
|
||||||
|
response.StatusCode = 200;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"Error processing job: {ex.Message}");
|
||||||
|
response.StatusCode = 400; // Bad Request
|
||||||
|
var errorResponse = new { error = $"Error processing request: {ex.Message}" };
|
||||||
|
await response.WriteAsJsonAsync(errorResponse);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
app.Run($"http://localhost:{appPort}");
|
||||||
|
|
||||||
|
static DroidJob SetDroidJob(string droidStr)
|
||||||
|
{
|
||||||
|
var parts = droidStr.Split(":");
|
||||||
|
if (parts.Length != 2)
|
||||||
|
{
|
||||||
|
throw new Exception("Invalid droid job format. Expected format: 'Droid:Task'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new DroidJob
|
||||||
|
{
|
||||||
|
Droid = parts[0],
|
||||||
|
Task = parts[1]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Classes for request and response models
|
||||||
|
public class JobData
|
||||||
|
{
|
||||||
|
public string? Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DroidJob
|
||||||
|
{
|
||||||
|
public string? Droid { get; set; }
|
||||||
|
public string? Task { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||||
|
"iisSettings": {
|
||||||
|
"windowsAuthentication": false,
|
||||||
|
"anonymousAuthentication": true,
|
||||||
|
"iisExpress": {
|
||||||
|
"applicationUrl": "http://localhost:5305",
|
||||||
|
"sslPort": 44346
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "http://localhost:5023",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": true,
|
||||||
|
"applicationUrl": "https://localhost:7073;http://localhost:5023",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"IIS Express": {
|
||||||
|
"commandName": "IISExpress",
|
||||||
|
"launchBrowser": true,
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>job_service</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
include ../../../docker.mk
|
||||||
|
include ../../../validate.mk
|
||||||
Loading…
Reference in New Issue