diff options
| author | vincent van beek <vincent@vlogic.nl> | 2026-04-15 16:19:02 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-04-15 16:19:02 +0200 |
| commit | 11e355321db20b70c76c35b6e8fc36dbb9d97fc6 (patch) | |
| tree | f12b8c8c2b6a642b315f2e4a7e54274bbcdb60be /opendc-web/opendc-web-server/src/main/java/org | |
| parent | 3e52cd36bed9455105f4a8c3d83ec805c1fb7b70 (diff) | |
add a job report to the scenario overview with details and time data (#406)
* add a job report to the scenario overview with details and time data
* create Report data class
Diffstat (limited to 'opendc-web/opendc-web-server/src/main/java/org')
5 files changed, 88 insertions, 15 deletions
diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java index 63982854..b78815da 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java @@ -67,6 +67,12 @@ public class Job extends PanacheEntityBase { public Instant createdAt; /** + * The instant at which the job started running. + */ + @Column(name = "started_at") + public Instant startedAt; + + /** * The number of simulation runs to perform. */ @Column(nullable = false, updatable = false) @@ -99,6 +105,13 @@ public class Job extends PanacheEntityBase { public Map<String, ?> results = null; /** + * Job report containing warnings and errors in JSON + */ + @Column(columnDefinition = "jsonb") + @Type(JsonType.class) + public Map<String, Object> report = null; + + /** * Construct a {@link Job} instance. */ public Job(Scenario scenario, String createdBy, Instant createdAt, int repeats) { @@ -129,15 +142,28 @@ public class Job extends PanacheEntityBase { * * @param newState The new state to enter into. * @param time The time at which the update occurs. + * @param startedAt The time at which the job started running (optional). + * @param runtime The runtime (in seconds) consumed by the simulation job so far. * @param results The results to possible set. + * @param report The report to possible set. * @return <code>true</code> when the update succeeded`, <code>false</code> when there was a conflict. */ - public boolean updateAtomically(JobState newState, Instant time, int runtime, Map<String, ?> results) { + public boolean updateAtomically( + JobState newState, + Instant time, + Instant startedAt, + int runtime, + Map<String, ?> results, + Map<String, Object> report) { // Update entity fields directly - this uses the JsonType converter for proper JSON serialization this.state = newState; this.updatedAt = time; + if (startedAt != null) { + this.startedAt = startedAt; + } this.runtime = runtime; this.results = results; + this.report = report; // Flush changes to database - JsonType will properly serialize the results map to JSON Panache.getEntityManager().flush(); diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java index 2b774082..b0b6274f 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/JobResource.java @@ -22,6 +22,7 @@ package org.opendc.web.server.rest.runner; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.security.RolesAllowed; import jakarta.transaction.Transactional; import jakarta.validation.Valid; @@ -33,6 +34,7 @@ import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.WebApplicationException; import java.util.List; +import java.util.Map; import org.opendc.web.proto.JobState; import org.opendc.web.server.model.Job; import org.opendc.web.server.service.JobService; @@ -49,13 +51,17 @@ public final class JobResource { */ private final JobService jobService; + private final ObjectMapper objectMapper; + /** * Construct a {@link JobResource} instance. * * @param jobService The {@link JobService} for managing the job lifecycle. + * @param objectMapper The {@link ObjectMapper} for JSON conversions. */ - public JobResource(JobService jobService) { + public JobResource(JobService jobService, ObjectMapper objectMapper) { this.jobService = jobService; + this.objectMapper = objectMapper; } /** @@ -98,7 +104,10 @@ public final class JobResource { } try { - jobService.updateJob(job, update.state(), update.runtime(), update.results()); + @SuppressWarnings("unchecked") + Map<String, Object> reportMap = + update.report() != null ? objectMapper.convertValue(update.report(), Map.class) : null; + jobService.updateJob(job, update.state(), update.runtime(), update.results(), reportMap); } catch (IllegalArgumentException e) { throw new WebApplicationException(e, 400); } catch (IllegalStateException e) { @@ -107,4 +116,19 @@ public final class JobResource { return RunnerProtocol.toDto(job); } + + /** + * Get the report for a job. + */ + @GET + @Path("{job}/report") + public Map<String, Object> getReport(@PathParam("job") long id) { + Job job = Job.findById(id); + + if (job == null) { + throw new WebApplicationException("Job not found", 404); + } + + return job.report != null ? job.report : Map.of(); + } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java index 6bf65d97..4c03aeea 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/rest/runner/RunnerProtocol.java @@ -42,7 +42,14 @@ public final class RunnerProtocol { */ public static org.opendc.web.proto.runner.Job toDto(Job job) { return new org.opendc.web.proto.runner.Job( - job.id, toDto(job.scenario), job.state, job.createdAt, job.updatedAt, job.runtime, job.results); + job.id, + toDto(job.scenario), + job.state, + job.createdAt, + job.startedAt, + job.updatedAt, + job.runtime, + job.results); } /** diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java index 70933520..1f8ac68e 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java @@ -55,10 +55,11 @@ public final class JobService { * @param newState The new state to transition the job to. * @param runtime The runtime (in seconds) consumed by the simulation jbo so far. * @param results The results to attach to the job. + * @param report The report containing warnings and errors. * @throws IllegalArgumentException if the state transition is invalid. * @throws IllegalStateException if someone tries to update the job concurrently. */ - public void updateJob(Job job, JobState newState, int runtime, Map<String, ?> results) { + public void updateJob(Job job, JobState newState, int runtime, Map<String, ?> results, Map<String, Object> report) { JobState state = job.state; if (!job.canTransitionTo(newState)) { @@ -69,12 +70,18 @@ public final class JobService { JobState nextState = newState; int consumedBudget = Math.min(1, runtime - job.runtime); + // Set startedAt when transitioning to RUNNING for the first time + Instant startedAt = job.startedAt; + if (newState == JobState.RUNNING && job.state != JobState.RUNNING && startedAt == null) { + startedAt = now; + } + // Check whether the user still has any simulation budget left if (accountingService.consumeSimulationBudget(job.createdBy, consumedBudget) && nextState == JobState.RUNNING) { nextState = JobState.FAILED; // User has consumed all their budget; cancel the job } - if (!job.updateAtomically(nextState, now, runtime, results)) { + if (!job.updateAtomically(nextState, now, startedAt, runtime, results, report)) { throw new IllegalStateException("Conflicting update"); } } diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java index 47d397f3..df9f7d97 100644 --- a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/util/runner/QuarkusJobManager.java @@ -22,12 +22,14 @@ package org.opendc.web.server.util.runner; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.enterprise.context.ApplicationScoped; import jakarta.transaction.Transactional; import java.util.Map; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.opendc.web.proto.JobState; +import org.opendc.web.proto.runner.Report; import org.opendc.web.runner.JobManager; import org.opendc.web.server.model.Job; import org.opendc.web.server.rest.runner.RunnerProtocol; @@ -43,13 +45,17 @@ public class QuarkusJobManager implements JobManager { */ private final JobService jobService; + private final ObjectMapper objectMapper; + /** * Construct a {@link QuarkusJobManager}. * * @param jobService The {@link JobService} for managing the job's lifecycle. + * @param objectMapper The {@link ObjectMapper} for JSON conversions. */ - public QuarkusJobManager(JobService jobService) { + public QuarkusJobManager(JobService jobService, ObjectMapper objectMapper) { this.jobService = jobService; + this.objectMapper = objectMapper; } @Transactional @@ -67,25 +73,25 @@ public class QuarkusJobManager implements JobManager { @Transactional @Override public boolean claim(long id) { - return updateState(id, JobState.CLAIMED, 0, null); + return updateState(id, JobState.CLAIMED, 0, null, null); } @Transactional @Override public boolean heartbeat(long id, int runtime) { - return updateState(id, JobState.RUNNING, runtime, null); + return updateState(id, JobState.RUNNING, runtime, null, null); } @Transactional @Override - public void fail(long id, int runtime) { - updateState(id, JobState.FAILED, runtime, null); + public void fail(long id, int runtime, @Nullable Report report) { + updateState(id, JobState.FAILED, runtime, null, report); } @Transactional @Override - public void finish(long id, int runtime, @NotNull Map<String, ?> results) { - updateState(id, JobState.FINISHED, runtime, results); + public void finish(long id, int runtime, @NotNull Map<String, ? extends Object> results, @Nullable Report report) { + updateState(id, JobState.FINISHED, runtime, results, report); } /** @@ -95,9 +101,10 @@ public class QuarkusJobManager implements JobManager { * @param newState The new state to transition to. * @param runtime The runtime of the job. * @param results The results of the job. + * @param report The report containing warnings and errors. * @return <code>true</code> if the operation succeeded, <code>false</code> otherwise. */ - private boolean updateState(long id, JobState newState, int runtime, Map<String, ?> results) { + private boolean updateState(long id, JobState newState, int runtime, Map<String, ?> results, Report report) { Job job = Job.findById(id); if (job == null) { @@ -105,7 +112,9 @@ public class QuarkusJobManager implements JobManager { } try { - jobService.updateJob(job, newState, runtime, results); + @SuppressWarnings("unchecked") + Map<String, Object> reportMap = report != null ? objectMapper.convertValue(report, Map.class) : null; + jobService.updateJob(job, newState, runtime, results, reportMap); return true; } catch (IllegalArgumentException | IllegalStateException e) { return false; |
