Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package au.org.aodn.ogcapi.server.core.configuration;

import au.org.aodn.ogcapi.server.processes.RestServices;
import au.org.aodn.ogcapi.server.processes.BatchJobProperties;
import au.org.aodn.ogcapi.server.processes.DownloadJobStatusAggregator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
Expand All @@ -15,12 +17,6 @@ public class AwsConfig {
@Value("${aws.region}")
private String awsRegion;

@Value("${aws.batch.job.definition}")
private String batchJobDefinition;

@Value("${aws.batch.job.queue}")
private String batchJobQueue;

@Bean
public BatchClient batchClient() {
return BatchClient
Expand All @@ -31,7 +27,15 @@ public BatchClient batchClient() {
}

@Bean
public RestServices awsBatchService(BatchClient batchClient, ObjectMapper objectMapper) {
return new RestServices(batchClient, objectMapper, batchJobDefinition, batchJobQueue);
public RestServices awsBatchService(
BatchClient batchClient,
ObjectMapper objectMapper,
BatchJobProperties properties) {
return new RestServices(batchClient, objectMapper, properties.definition(), properties.queue());
}

@Bean
public DownloadJobStatusAggregator downloadJobStatusAggregator() {
return new DownloadJobStatusAggregator();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import au.org.aodn.ogcapi.server.core.util.ConstructUtils;
import au.org.aodn.ogcapi.server.core.util.GeometryUtils;
import au.org.aodn.ogcapi.server.core.util.RestTemplateUtils;
import au.org.aodn.ogcapi.server.processes.BatchJobProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
Expand All @@ -32,7 +33,8 @@
DdaProperties.class,
IndexerProperties.class,
GNProperties.class,
DasProperties.class
DasProperties.class,
BatchJobProperties.class
})
public class Config {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package au.org.aodn.ogcapi.server.core.exception;

public class DownloadJobNotFoundException extends RuntimeException {
public DownloadJobNotFoundException() {
super("Download job not found");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package au.org.aodn.ogcapi.server.core.exception;

public class DownloadJobStatusException extends RuntimeException {
public DownloadJobStatusException() {
super("Unable to retrieve download job status");
}

public DownloadJobStatusException(Throwable cause) {
super("Unable to retrieve download job status", cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,34 @@ public ResponseEntity<ErrorResponse> handleResourceNotFoundException(ResourceNot
return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}

@ExceptionHandler(DownloadJobNotFoundException.class)
public ResponseEntity<ErrorResponse> handleDownloadJobNotFoundException(
DownloadJobNotFoundException ex,
WebRequest request) {
ErrorResponse errorResponse = ErrorResponse
.builder()
.timestamp(LocalDateTime.now())
.message(ex.getMessage())
.details(request.getDescription(false))
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}

@ExceptionHandler(DownloadJobStatusException.class)
public ResponseEntity<ErrorResponse> handleDownloadJobStatusException(
DownloadJobStatusException ex,
WebRequest request) {
ErrorResponse errorResponse = ErrorResponse
.builder()
.timestamp(LocalDateTime.now())
.message(ex.getMessage())
.details(request.getDescription(false))
.build();

return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(DasUpstreamException.class)
public ResponseEntity<ErrorResponse> handleDasUpstreamException(DasUpstreamException ex, WebRequest request) {
ErrorResponse errorResponse = ErrorResponse
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package au.org.aodn.ogcapi.server.core.model;

import au.org.aodn.ogcapi.processes.model.InlineResponse200;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;

@Schema(description = "Compatible download execution response with the submitted AWS Batch job ID.")
public record DownloadExecutionResponse(
Comment thread
utas-raymondng marked this conversation as resolved.
@JsonProperty("message") InlineValue message,
@JsonProperty("status") InlineValue status,
@JsonProperty("jobID") String jobId
) implements InlineResponse200 {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package au.org.aodn.ogcapi.server.core.model;

import au.org.aodn.ogcapi.processes.model.StatusInfo;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;

/**
* AODN display metadata added to the standard OGC job status response.
*/
@Schema(description = "AODN download job status, extending the standard OGC StatusInfo model.")
public class DownloadJobStatusInfo extends StatusInfo {

@JsonProperty("collection")
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@Schema(description = "Display name of the requested collection.")
private String collection;

@JsonProperty("dataSelection")
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@Schema(description = "Dataset key or data selection requested for the download.")
private String dataSelection;

@JsonProperty("format")
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@Schema(description = "Requested output document format.")
private String format;

@JsonProperty("metadataUrl")
@JsonInclude(JsonInclude.Include.NON_ABSENT)
@Schema(description = "Link to the metadata page supplied when the download was submitted.")
private String metadataUrl;

public String getCollection() {
return collection;
}

public void setCollection(String collection) {
this.collection = collection;
}

public String getDataSelection() {
return dataSelection;
}

public void setDataSelection(String dataSelection) {
this.dataSelection = dataSelection;
}

public String getFormat() {
return format;
}

public void setFormat(String format) {
this.format = format;
}

public String getMetadataUrl() {
return metadataUrl;
}

public void setMetadataUrl(String metadataUrl) {
this.metadataUrl = metadataUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
public enum InlineResponseKeyEnum {
MESSAGE("message"),
STATUS("status"),
JOB_ID("jobID"),
;
private final String value;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package au.org.aodn.ogcapi.server.processes;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "aws.batch.job")
public record BatchJobProperties(
String queue,
String definition,
String childQueue
) {
public BatchJobProperties {
if (childQueue == null || childQueue.isBlank()) {
childQueue = queue;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package au.org.aodn.ogcapi.server.processes;

import au.org.aodn.ogcapi.processes.model.StatusCode;
import software.amazon.awssdk.services.batch.model.JobStatus;

import java.util.Objects;
import java.util.stream.Stream;

/**
* Maps an already-fetched AWS Batch workflow snapshot to its public OGC status.
* This class deliberately makes no AWS calls so the workflow rules remain deterministic.
*/
public class DownloadJobStatusAggregator {

public enum WorkflowMode {
EXPLICIT_ZARR,
CHILD_DISCOVERY_REQUIRED
}

public record Snapshot(
JobStatus initial,
JobStatus prepare,
JobStatus collect,
WorkflowMode workflowMode,
boolean discoveryWindowExpired
) {
public Snapshot {
Objects.requireNonNull(initial, "initial status is required");
Objects.requireNonNull(workflowMode, "workflow mode is required");
}
}

public StatusCode aggregate(Snapshot snapshot) {
if (Stream.of(snapshot.initial(), snapshot.prepare(), snapshot.collect())
.anyMatch(status -> status == JobStatus.FAILED)) {
return StatusCode.FAILED;
}

if (Stream.of(snapshot.initial(), snapshot.prepare(), snapshot.collect())
.filter(Objects::nonNull)
.anyMatch(status -> status == JobStatus.UNKNOWN_TO_SDK_VERSION)) {
throw new IllegalStateException("Unsupported AWS Batch job status");
}

boolean hasPrepare = snapshot.prepare() != null;
boolean hasCollect = snapshot.collect() != null;
if (hasPrepare != hasCollect) {
if (snapshot.discoveryWindowExpired()) {
throw new IllegalStateException("Only one child workflow job was found after the discovery window");
}
return StatusCode.RUNNING;
}

if (snapshot.collect() == JobStatus.SUCCEEDED) {
return StatusCode.SUCCESSFUL;
}

if (hasPrepare) {
return StatusCode.RUNNING;
}

return switch (snapshot.initial()) {
case SUBMITTED, PENDING, RUNNABLE -> StatusCode.ACCEPTED;
case STARTING, RUNNING -> StatusCode.RUNNING;
case SUCCEEDED -> {
if (snapshot.workflowMode() == WorkflowMode.EXPLICIT_ZARR
|| snapshot.discoveryWindowExpired()) {
yield StatusCode.SUCCESSFUL;
}
yield StatusCode.RUNNING;
}
case FAILED -> StatusCode.FAILED;
default -> throw new IllegalStateException("Unsupported AWS Batch job status");
};
}
}
Loading
Loading