-
Notifications
You must be signed in to change notification settings - Fork 53
Export History job #494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Export History job #494
Changes from all commits
871eefe
c0b946b
13c0dcb
52bf21c
30fcebc
ced2c4b
5749aeb
46b01e4
2df6b53
208aade
00ee88d
b45b151
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk.Web"> | ||
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net6.0</TargetFramework> | ||
| <Nullable>enable</Nullable> | ||
| <ImplicitUsings>enable</ImplicitUsings> | ||
| <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> | ||
| <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Azure.Identity" /> | ||
| <PackageReference Include="Grpc.Net.Client" /> | ||
| <PackageReference Include="Microsoft.DurableTask.Generators" OutputItemType="Analyzer" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <ProjectReference Include="..\..\src\Client\AzureManaged\Client.AzureManaged.csproj" /> | ||
| <ProjectReference Include="..\..\src\Worker\AzureManaged\Worker.AzureManaged.csproj" /> | ||
| <ProjectReference Include="..\..\src\ExportHistory\ExportHistory.csproj" /> | ||
| </ItemGroup> | ||
| </Project> | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| ### Variables | ||
| @baseUrl = http://localhost:5010 | ||
| @jobId = export-job-12345 | ||
|
|
||
| ### Create a new batch export job | ||
| # @name createBatchExportJob | ||
| POST {{baseUrl}}/export-jobs | ||
| Content-Type: application/json | ||
|
|
||
| { | ||
| "jobId": "{{jobId}}", | ||
| "mode": "Batch", | ||
| "completedTimeFrom": "2025-10-01T00:00:00Z", | ||
| "completedTimeTo": "2025-11-06T23:59:59Z", | ||
| "container": "export-history", | ||
| # "prefix": "exports/", | ||
| "maxInstancesPerBatch": 1, | ||
| "runtimeStatus": [] | ||
| } | ||
|
|
||
| ### Create a new continuous export job | ||
| # @name createContinuousExportJob | ||
| POST {{baseUrl}}/export-jobs | ||
| Content-Type: application/json | ||
|
|
||
| { | ||
| "jobId": "export-job-continuous-123", | ||
| "mode": "Continuous", | ||
| "container": "export-history", | ||
| # "prefix": "continuous-exports/", | ||
| "maxInstancesPerBatch": 1000 | ||
| # "runtimeStatus": ["asdasd"] | ||
| } | ||
|
|
||
| ### Create an export job with default storage (no container specified) | ||
| # @name createExportJobWithDefaultStorage | ||
| POST {{baseUrl}}/export-jobs | ||
| Content-Type: application/json | ||
| { | ||
| "jobId": "export-job-default-storage", | ||
| "mode": "Batch", | ||
| "completedTimeFrom": "2024-01-01T00:00:00Z", | ||
| "completedTimeTo": "2024-12-31T23:59:59Z", | ||
| "maxInstancesPerBatch": 100 | ||
| } | ||
|
|
||
| ### Get a specific export job by ID | ||
| # Note: This endpoint can be used to verify the export job was created and check its status | ||
| # The ID in the URL should match the jobId used in create request | ||
| GET {{baseUrl}}/export-jobs/{{jobId}} | ||
|
|
||
| ### List all export jobs | ||
| GET {{baseUrl}}/export-jobs/list | ||
|
|
||
| ### List export jobs with filters | ||
| ### Filter by status | ||
| GET {{baseUrl}}/export-jobs/list?status=Active | ||
|
|
||
| ### Filter by job ID prefix | ||
| GET {{baseUrl}}/export-jobs/list?jobIdPrefix=export-job- | ||
|
|
||
| ### Filter by creation time range | ||
| GET {{baseUrl}}/export-jobs/list?createdFrom=2024-01-01T00:00:00Z&createdTo=2024-12-31T23:59:59Z | ||
|
|
||
| ### Combined filters | ||
| GET {{baseUrl}}/export-jobs/list?status=Completed&jobIdPrefix=export-job-&pageSize=50 | ||
|
|
||
| ### Delete an export job | ||
| # DELETE {{baseUrl}}/export-jobs/{{jobId}} | ||
|
|
||
| # Delete a continuous export job | ||
| DELETE {{baseUrl}}/export-jobs/export-job-continuous-123jk | ||
|
|
||
| ### Tips: | ||
| # - Replace the baseUrl variable if your application runs on a different port | ||
| # - The jobId variable can be changed to test different export job instances | ||
| # - Export modes: | ||
| # - "Batch": Exports all instances within a time range (requires completedTimeTo) | ||
| # - "Continuous": Continuously exports instances from a start time (completedTimeTo must be null) | ||
| # - Runtime status filters (valid values): | ||
| # - "Completed": Exports only completed orchestrations | ||
| # - "Failed": Exports only failed orchestrations | ||
| # - "Terminated": Exports only terminated orchestrations | ||
| # - "ContinuedAsNew": Exports only continued-as-new orchestrations | ||
| # - Dates are in ISO 8601 format (YYYY-MM-DDThh:mm:ssZ) | ||
| # - You can use the REST Client extension in VS Code to execute these requests | ||
| # - The @name directive allows referencing the response in subsequent requests | ||
| # - Export jobs run asynchronously; use GET to check the status after creation | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,202 @@ | ||
| // Copyright (c) Microsoft Corporation. | ||
| // Licensed under the MIT License. | ||
|
|
||
| using Microsoft.AspNetCore.Mvc; | ||
| using Microsoft.DurableTask; | ||
| using Microsoft.DurableTask.Client; | ||
| using Microsoft.DurableTask.ExportHistory; | ||
| using ExportHistoryWebApp.Models; | ||
|
|
||
| namespace ExportHistoryWebApp.Controllers; | ||
|
|
||
| /// <summary> | ||
| /// Controller for managing export history jobs through a REST API. | ||
| /// Provides endpoints for creating, reading, listing, and deleting export jobs. | ||
| /// </summary> | ||
| [ApiController] | ||
| [Route("export-jobs")] | ||
| public class ExportJobController : ControllerBase | ||
| { | ||
| readonly ExportHistoryClient exportHistoryClient; | ||
| readonly ILogger<ExportJobController> logger; | ||
|
|
||
| /// <summary> | ||
| /// Initializes a new instance of the <see cref="ExportJobController"/> class. | ||
| /// </summary> | ||
| /// <param name="exportHistoryClient">Client for managing export history jobs.</param> | ||
| /// <param name="logger">Logger for recording controller operations.</param> | ||
| public ExportJobController( | ||
| ExportHistoryClient exportHistoryClient, | ||
| ILogger<ExportJobController> logger) | ||
| { | ||
| this.exportHistoryClient = exportHistoryClient ?? throw new ArgumentNullException(nameof(exportHistoryClient)); | ||
| this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Creates a new export job based on the provided configuration. | ||
| /// </summary> | ||
| /// <param name="request">The export job creation request.</param> | ||
| /// <returns>The created export job description.</returns> | ||
| [HttpPost] | ||
| public async Task<ActionResult<ExportJobDescription>> CreateExportJob([FromBody] CreateExportJobRequest request) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think we should have users define this or do you think we should have users use the |
||
| { | ||
| if (request == null) | ||
| { | ||
| return this.BadRequest("createExportJobRequest cannot be null"); | ||
| } | ||
|
|
||
| try | ||
| { | ||
| ExportDestination? destination = null; | ||
| if (!string.IsNullOrEmpty(request.Container)) | ||
| { | ||
| destination = new ExportDestination(request.Container) | ||
| { | ||
| Prefix = request.Prefix, | ||
| }; | ||
| } | ||
|
|
||
| ExportJobCreationOptions creationOptions = new ExportJobCreationOptions( | ||
| mode: request.Mode, | ||
| completedTimeFrom: request.CompletedTimeFrom, | ||
| completedTimeTo: request.CompletedTimeTo, | ||
| destination: destination, | ||
| jobId: request.JobId, | ||
| format: request.Format, | ||
| runtimeStatus: request.RuntimeStatus, | ||
| maxInstancesPerBatch: request.MaxInstancesPerBatch); | ||
|
|
||
| ExportHistoryJobClient jobClient = await this.exportHistoryClient.CreateJobAsync(creationOptions); | ||
| ExportJobDescription description = await jobClient.DescribeAsync(); | ||
|
|
||
| this.logger.LogInformation("Created new export job with ID: {JobId}", description.JobId); | ||
|
|
||
| return this.CreatedAtAction(nameof(GetExportJob), new { id = description.JobId }, description); | ||
| } | ||
| catch (ArgumentException ex) | ||
| { | ||
| this.logger.LogError(ex, "Validation failed while creating export job {JobId}", request.JobId); | ||
YunchuWang marked this conversation as resolved.
Dismissed
Show dismissed
Hide dismissed
|
||
| return this.BadRequest(ex.Message); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| this.logger.LogError(ex, "Error creating export job {JobId}", request.JobId); | ||
YunchuWang marked this conversation as resolved.
Dismissed
Show dismissed
Hide dismissed
|
||
| return this.StatusCode(500, "An error occurred while creating the export job"); | ||
| } | ||
YunchuWang marked this conversation as resolved.
Show resolved
Hide resolved
YunchuWang marked this conversation as resolved.
Show resolved
Hide resolved
YunchuWang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /// <summary> | ||
| /// Retrieves a specific export job by its ID. | ||
| /// </summary> | ||
| /// <param name="id">The ID of the export job to retrieve.</param> | ||
| /// <returns>The export job description if found.</returns> | ||
| [HttpGet("{id}")] | ||
| public async Task<ActionResult<ExportJobDescription>> GetExportJob(string id) | ||
| { | ||
| try | ||
| { | ||
| ExportJobDescription? job = await this.exportHistoryClient.GetJobAsync(id); | ||
| return this.Ok(job); | ||
| } | ||
| catch (ExportJobNotFoundException) | ||
| { | ||
| return this.NotFound(); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
YunchuWang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| this.logger.LogError(ex, "Error retrieving export job {JobId}", id); | ||
YunchuWang marked this conversation as resolved.
Dismissed
Show dismissed
Hide dismissed
|
||
| return this.StatusCode(500, "An error occurred while retrieving the export job"); | ||
| } | ||
YunchuWang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /// <summary> | ||
| /// Lists all export jobs, optionally filtered by query parameters. | ||
| /// </summary> | ||
| /// <param name="status">Optional filter by job status.</param> | ||
| /// <param name="jobIdPrefix">Optional filter by job ID prefix.</param> | ||
| /// <param name="createdFrom">Optional filter for jobs created after this time.</param> | ||
| /// <param name="createdTo">Optional filter for jobs created before this time.</param> | ||
| /// <param name="pageSize">Optional page size for pagination.</param> | ||
| /// <param name="continuationToken">Optional continuation token for pagination.</param> | ||
| /// <returns>A collection of export job descriptions.</returns> | ||
| [HttpGet("list")] | ||
| public async Task<ActionResult<IEnumerable<ExportJobDescription>>> ListExportJobs( | ||
| [FromQuery] ExportJobStatus? status = null, | ||
| [FromQuery] string? jobIdPrefix = null, | ||
| [FromQuery] DateTimeOffset? createdFrom = null, | ||
| [FromQuery] DateTimeOffset? createdTo = null, | ||
| [FromQuery] int? pageSize = null, | ||
| [FromQuery] string? continuationToken = null) | ||
| { | ||
| this.logger.LogInformation("GET list endpoint called with method: {Method}", this.HttpContext.Request.Method); | ||
YunchuWang marked this conversation as resolved.
Dismissed
Show dismissed
Hide dismissed
YunchuWang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| try | ||
| { | ||
| ExportJobQuery? query = null; | ||
| if ( | ||
| status.HasValue || | ||
| !string.IsNullOrEmpty(jobIdPrefix) || | ||
| createdFrom.HasValue || | ||
| createdTo.HasValue || | ||
| pageSize.HasValue || | ||
| !string.IsNullOrEmpty(continuationToken) | ||
| ) | ||
| { | ||
| query = new ExportJobQuery | ||
| { | ||
| Status = status, | ||
| JobIdPrefix = jobIdPrefix, | ||
| CreatedFrom = createdFrom, | ||
| CreatedTo = createdTo, | ||
| PageSize = pageSize, | ||
| ContinuationToken = continuationToken, | ||
| }; | ||
| } | ||
|
|
||
| AsyncPageable<ExportJobDescription> jobs = this.exportHistoryClient.ListJobsAsync(query); | ||
|
|
||
| // Collect all jobs from the async pageable | ||
| List<ExportJobDescription> jobList = new List<ExportJobDescription>(); | ||
| await foreach (ExportJobDescription job in jobs) | ||
| { | ||
| jobList.Add(job); | ||
| } | ||
|
|
||
| return this.Ok(jobList); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| this.logger.LogError(ex, "Error retrieving export jobs"); | ||
| return this.StatusCode(500, "An error occurred while retrieving export jobs"); | ||
| } | ||
YunchuWang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /// <summary> | ||
| /// Deletes an export job by its ID. | ||
| /// </summary> | ||
| /// <param name="id">The ID of the export job to delete.</param> | ||
| /// <returns>No content if successful.</returns> | ||
| [HttpDelete("{id}")] | ||
| public async Task<IActionResult> DeleteExportJob(string id) | ||
| { | ||
| this.logger.LogInformation("DELETE endpoint called for job ID: {JobId}", id); | ||
YunchuWang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| try | ||
| { | ||
| ExportHistoryJobClient jobClient = this.exportHistoryClient.GetJobClient(id); | ||
| await jobClient.DeleteAsync(); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. From a user perspective, is this deleting an old job, or is this how you stop an ongoing job? If it's for stopping, we should update the API name. If it's strictly like a purge, we can keep it, but maybe use the same terminology from the existing APIs.
Comment on lines
+185
to
+186
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need the pattern of getting a dedicated client for this? Or do you think we could wrap this into the
Comment on lines
+185
to
+186
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think we really need both the |
||
| this.logger.LogInformation("Successfully deleted export job {JobId}", id); | ||
YunchuWang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return this.NoContent(); | ||
| } | ||
| catch (ExportJobNotFoundException) | ||
| { | ||
| this.logger.LogWarning("Export job {JobId} not found for deletion", id); | ||
YunchuWang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| return this.NotFound(); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| this.logger.LogError(ex, "Error deleting export job {JobId}", id); | ||
YunchuWang marked this conversation as resolved.
Dismissed
Show dismissed
Hide dismissed
|
||
| return this.StatusCode(500, "An error occurred while deleting the export job"); | ||
YunchuWang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
YunchuWang marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of having users create their own request, should we just show them using the options here as the domain object? I want to make sure we set the correct precedent for them.