Skip to content

Commit

Permalink
HTTP csharp jobs quickstart
Browse files Browse the repository at this point in the history
Signed-off-by: Alice Gibbons <[email protected]>
  • Loading branch information
alicejgibbons committed Jan 20, 2025
1 parent 4818dda commit 8def820
Show file tree
Hide file tree
Showing 10 changed files with 425 additions and 0 deletions.
181 changes: 181 additions & 0 deletions jobs/csharp/http/README.md
Original file line number Diff line number Diff line change
@@ -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
```
13 changes: 13 additions & 0 deletions jobs/csharp/http/dapr.yaml
Original file line number Diff line number Diff line change
@@ -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"]
78 changes: 78 additions & 0 deletions jobs/csharp/http/job-scheduler/Program.cs
Original file line number Diff line number Diff line change
@@ -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}");
}
11 changes: 11 additions & 0 deletions jobs/csharp/http/job-scheduler/jobs-scheduler.csproj
Original file line number Diff line number Diff line change
@@ -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>
75 changes: 75 additions & 0 deletions jobs/csharp/http/job-service/Program.cs
Original file line number Diff line number Diff line change
@@ -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; }
}
Loading

0 comments on commit 8def820

Please sign in to comment.