diff --git a/jobs/csharp/http/README.md b/jobs/csharp/http/README.md new file mode 100644 index 000000000..4d4370bae --- /dev/null +++ b/jobs/csharp/http/README.md @@ -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: + + + +```bash +cd ./job-service +dotnet build +``` + + + + + +```bash +cd ./job-scheduler +dotnet build +``` + + + +2. Run the multi app run template: + + + +```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 +``` + + + +## 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 +``` diff --git a/jobs/csharp/http/dapr.yaml b/jobs/csharp/http/dapr.yaml new file mode 100644 index 000000000..f52271b71 --- /dev/null +++ b/jobs/csharp/http/dapr.yaml @@ -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"] \ No newline at end of file diff --git a/jobs/csharp/http/job-scheduler/Program.cs b/jobs/csharp/http/job-scheduler/Program.cs new file mode 100644 index 000000000..86697f357 --- /dev/null +++ b/jobs/csharp/http/job-scheduler/Program.cs @@ -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}"); +} diff --git a/jobs/csharp/http/job-scheduler/jobs-scheduler.csproj b/jobs/csharp/http/job-scheduler/jobs-scheduler.csproj new file mode 100644 index 000000000..9a93a9ffc --- /dev/null +++ b/jobs/csharp/http/job-scheduler/jobs-scheduler.csproj @@ -0,0 +1,11 @@ + + + + Exe + net8.0 + jobs_scheduler + enable + enable + + + diff --git a/jobs/csharp/http/job-service/Program.cs b/jobs/csharp/http/job-service/Program.cs new file mode 100644 index 000000000..e016dbc5c --- /dev/null +++ b/jobs/csharp/http/job-service/Program.cs @@ -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(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(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; } +} diff --git a/jobs/csharp/http/job-service/Properties/launchSettings.json b/jobs/csharp/http/job-service/Properties/launchSettings.json new file mode 100644 index 000000000..22eea4023 --- /dev/null +++ b/jobs/csharp/http/job-service/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/jobs/csharp/http/job-service/appsettings.Development.json b/jobs/csharp/http/job-service/appsettings.Development.json new file mode 100644 index 000000000..ff66ba6b2 --- /dev/null +++ b/jobs/csharp/http/job-service/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/jobs/csharp/http/job-service/appsettings.json b/jobs/csharp/http/job-service/appsettings.json new file mode 100644 index 000000000..4d566948d --- /dev/null +++ b/jobs/csharp/http/job-service/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/jobs/csharp/http/job-service/job-service.csproj b/jobs/csharp/http/job-service/job-service.csproj new file mode 100644 index 000000000..b953f863d --- /dev/null +++ b/jobs/csharp/http/job-service/job-service.csproj @@ -0,0 +1,10 @@ + + + + net8.0 + enable + enable + job_service + + + diff --git a/jobs/csharp/http/makefile b/jobs/csharp/http/makefile new file mode 100644 index 000000000..e7a8826bf --- /dev/null +++ b/jobs/csharp/http/makefile @@ -0,0 +1,2 @@ +include ../../../docker.mk +include ../../../validate.mk \ No newline at end of file