From e64487cb57ca75d17fe5a8a664c1e8247c7b5168 Mon Sep 17 00:00:00 2001 From: Fabian Mastenbroek Date: Sat, 7 Jan 2023 19:23:11 +0000 Subject: refactor(web/server): Use Panache for entity modeling This change updates the OpenDC web server to use Panache (provided by Quarkus) to model entities. Such approach is better supported in Quarkus and simplifies our implementation. --- .../main/java/org/opendc/web/server/model/Job.java | 142 +++++++++++++ .../org/opendc/web/server/model/Portfolio.java | 135 +++++++++++++ .../java/org/opendc/web/server/model/Project.java | 224 +++++++++++++++++++++ .../web/server/model/ProjectAuthorization.java | 163 +++++++++++++++ .../java/org/opendc/web/server/model/Scenario.java | 189 +++++++++++++++++ .../java/org/opendc/web/server/model/Topology.java | 135 +++++++++++++ .../java/org/opendc/web/server/model/Trace.java | 70 +++++++ .../opendc/web/server/model/UserAccounting.java | 167 +++++++++++++++ .../java/org/opendc/web/server/model/Workload.java | 61 ++++++ .../org/opendc/web/server/service/JobService.java | 138 +++++++++++++ .../web/server/service/PortfolioService.java | 148 ++++++++++++++ .../opendc/web/server/service/ProjectService.java | 106 ++++++++++ .../opendc/web/server/service/ScenarioService.java | 224 +++++++++++++++++++++ .../opendc/web/server/service/TopologyService.java | 178 ++++++++++++++++ .../opendc/web/server/service/TraceService.java | 55 +++++ .../web/server/service/UserAccountingService.java | 136 +++++++++++++ .../org/opendc/web/server/service/UserService.java | 58 ++++++ .../web/server/util/runner/QuarkusJobManager.java | 2 +- 18 files changed, 2330 insertions(+), 1 deletion(-) create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Workload.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TraceService.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java create mode 100644 opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserService.java (limited to 'opendc-web/opendc-web-server/src/main/java') 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 new file mode 100644 index 00000000..14fd3e2a --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Job.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.panache.common.Parameters; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.JoinColumn; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import org.hibernate.annotations.Type; +import org.opendc.web.proto.JobState; + +/** + * A simulation job to be run by the simulator. + */ +@Entity +@Table(name = "jobs") +@NamedQueries({ + @NamedQuery( + name = "Job.updateOne", + query = + """ + UPDATE Job j + SET j.state = :newState, j.updatedAt = :updatedAt, j.runtime = :runtime, j.results = :results + WHERE j.id = :id AND j.state = :oldState + """) +}) +public class Job extends PanacheEntity { + @OneToOne(optional = false, mappedBy = "job", fetch = FetchType.EAGER) + @JoinColumn(name = "scenario_id", nullable = false) + public Scenario scenario; + + @Column(name = "created_by", nullable = false, updatable = false) + public String createdBy; + + @Column(name = "created_at", nullable = false, updatable = false) + public Instant createdAt; + + /** + * The number of simulation runs to perform. + */ + @Column(nullable = false, updatable = false) + public int repeats; + + /** + * The instant at which the job was updated. + */ + @Column(name = "updated_at", nullable = false) + public Instant updatedAt = createdAt; + + /** + * The state of the job. + */ + @Column(nullable = false) + public JobState state = JobState.PENDING; + + /** + * The runtime of the job (in seconds). + */ + @Column(nullable = false) + public int runtime = 0; + + /** + * Experiment results in JSON + */ + @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType") + @Column(columnDefinition = "jsonb") + public Map results = null; + + /** + * Construct a {@link Job} instance. + */ + public Job(Scenario scenario, String createdBy, Instant createdAt, int repeats) { + this.createdBy = createdBy; + this.scenario = scenario; + this.createdAt = createdAt; + this.repeats = repeats; + } + + /** + * JPA constructor + */ + protected Job() {} + + /** + * Find {@link Job}s in the specified {@link JobState}. + * + * @param state The state of the jobs to find. + * @return The list of jobs that are in the specified state. + */ + public static List findByState(JobState state) { + return find("state", state).list(); + } + + /** + * Atomically update this job. + * + * @param newState The new state to enter into. + * @param time The time at which the update occurs. + * @param results The results to possible set. + * @return true when the update succeeded`, false when there was a conflict. + */ + public boolean updateAtomically(JobState newState, Instant time, int runtime, Map results) { + long count = update( + "#Job.updateOne", + Parameters.with("id", id) + .and("oldState", state) + .and("newState", newState) + .and("updatedAt", time) + .and("runtime", runtime) + .and("results", results)); + return count > 0; + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java new file mode 100644 index 00000000..4c3af570 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Portfolio.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.panache.common.Parameters; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import org.hibernate.annotations.Type; +import org.opendc.web.proto.Targets; + +/** + * A portfolio is the composition of multiple scenarios. + */ +@Entity +@Table( + name = "portfolios", + uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, + indexes = {@Index(name = "fn_portfolios_number", columnList = "project_id, number")}) +@NamedQueries({ + @NamedQuery(name = "Portfolio.findByProject", query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId"), + @NamedQuery( + name = "Portfolio.findOneByProject", + query = "SELECT p FROM Portfolio p WHERE p.project.id = :projectId AND p.number = :number") +}) +public class Portfolio extends PanacheEntity { + /** + * The {@link Project} this portfolio belongs to. + */ + @ManyToOne(optional = false) + @JoinColumn(name = "project_id", nullable = false) + public Project project; + + /** + * Unique number of the portfolio for the project. + */ + @Column(nullable = false) + public int number; + + /** + * The name of this portfolio. + */ + @Column(nullable = false) + public String name; + + /** + * The portfolio targets (metrics, repetitions). + */ + @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType") + @Column(columnDefinition = "jsonb", nullable = false, updatable = false) + public Targets targets; + + /** + * The scenarios in this portfolio. + */ + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "portfolio", + orphanRemoval = true) + @OrderBy("id ASC") + public Set scenarios = new HashSet<>(); + + /** + * Construct a {@link Portfolio} object. + */ + public Portfolio(Project project, int number, String name, Targets targets) { + this.project = project; + this.number = number; + this.name = name; + this.targets = targets; + } + + /** + * JPA constructor + */ + protected Portfolio() {} + + /** + * Find all {@link Portfolio}s that belong to the specified project + * + * @param projectId The unique identifier of the project. + * @return The list of portfolios that belong to the specified project. + */ + public static List findByProject(long projectId) { + return find("#Portfolio.findByProject", Parameters.with("projectId", projectId)) + .list(); + } + + /** + * Find the {@link Portfolio} with the specified number belonging to the specified project. + * + * @param projectId The unique identifier of the project. + * @param number The number of the scenario. + * @return The portfolio or null if it does not exist. + */ + public static Portfolio findByProject(long projectId, int number) { + return find( + "#Portfolio.findOneByProject", + Parameters.with("projectId", projectId).and("number", number)) + .firstResult(); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java new file mode 100644 index 00000000..5836e33f --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Project.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.model; + +import io.quarkus.hibernate.orm.panache.Panache; +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.panache.common.Parameters; +import java.time.Instant; +import java.util.HashSet; +import java.util.Set; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToMany; +import javax.persistence.OrderBy; +import javax.persistence.Table; + +/** + * A project in OpenDC encapsulates all the datacenter designs and simulation runs for a set of users. + */ +@Entity +@Table(name = "projects") +@NamedQueries({ + @NamedQuery( + name = "Project.findByUser", + query = + """ + SELECT a + FROM ProjectAuthorization a + WHERE a.key.userId = :userId + """), + @NamedQuery( + name = "Project.allocatePortfolio", + query = + """ + UPDATE Project p + SET p.portfoliosCreated = :oldState + 1, p.updatedAt = :now + WHERE p.id = :id AND p.portfoliosCreated = :oldState + """), + @NamedQuery( + name = "Project.allocateTopology", + query = + """ + UPDATE Project p + SET p.topologiesCreated = :oldState + 1, p.updatedAt = :now + WHERE p.id = :id AND p.topologiesCreated = :oldState + """), + @NamedQuery( + name = "Project.allocateScenario", + query = + """ + UPDATE Project p + SET p.scenariosCreated = :oldState + 1, p.updatedAt = :now + WHERE p.id = :id AND p.scenariosCreated = :oldState + """) +}) +public class Project extends PanacheEntity { + /** + * The name of the project. + */ + @Column(nullable = false) + public String name; + + /** + * The instant at which the project was created. + */ + @Column(name = "created_at", nullable = false, updatable = false) + public Instant createdAt; + + /** + * The instant at which the project was updated. + */ + @Column(name = "updated_at", nullable = false) + public Instant updatedAt; + + /** + * The portfolios belonging to this project. + */ + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "project", + orphanRemoval = true) + @OrderBy("id ASC") + public Set portfolios = new HashSet<>(); + + /** + * The number of portfolios created for this project (including deleted portfolios). + */ + @Column(name = "portfolios_created", nullable = false) + public int portfoliosCreated = 0; + + /** + * The topologies belonging to this project. + */ + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "project", + orphanRemoval = true) + @OrderBy("id ASC") + public Set topologies = new HashSet<>(); + + /** + * The number of topologies created for this project (including deleted topologies). + */ + @Column(name = "topologies_created", nullable = false) + public int topologiesCreated = 0; + + /** + * The scenarios belonging to this project. + */ + @OneToMany(mappedBy = "project", orphanRemoval = true) + public Set scenarios = new HashSet<>(); + + /** + * The number of scenarios created for this project (including deleted scenarios). + */ + @Column(name = "scenarios_created", nullable = false) + public int scenariosCreated = 0; + + /** + * The users authorized to access the project. + */ + @OneToMany( + cascade = {CascadeType.ALL}, + mappedBy = "project", + orphanRemoval = true) + public Set authorizations = new HashSet<>(); + + /** + * Construct a {@link Project} object. + */ + public Project(String name, Instant createdAt) { + this.name = name; + this.createdAt = createdAt; + this.updatedAt = createdAt; + } + + /** + * JPA constructor + */ + protected Project() {} + + /** + * Allocate the next portfolio number for the specified [project]. + * + * @param time The time at which the new portfolio is created. + */ + public int allocatePortfolio(Instant time) { + for (int i = 0; i < 4; i++) { + long count = update( + "#Project.allocatePortfolio", + Parameters.with("id", id).and("oldState", portfoliosCreated).and("now", time)); + if (count > 0) { + return portfoliosCreated + 1; + } else { + Panache.getEntityManager().refresh(this); + } + } + + throw new IllegalStateException("Failed to allocate next portfolio"); + } + + /** + * Allocate the next topology number for the specified [project]. + * + * @param time The time at which the new topology is created. + */ + public int allocateTopology(Instant time) { + for (int i = 0; i < 4; i++) { + long count = update( + "#Project.allocateTopology", + Parameters.with("id", id).and("oldState", topologiesCreated).and("now", time)); + if (count > 0) { + return topologiesCreated + 1; + } else { + Panache.getEntityManager().refresh(this); + } + } + + throw new IllegalStateException("Failed to allocate next topology"); + } + + /** + * Allocate the next scenario number for the specified [project]. + * + * @param time The time at which the new scenario is created. + */ + public int allocateScenario(Instant time) { + for (int i = 0; i < 4; i++) { + long count = update( + "#Project.allocateScenario", + Parameters.with("id", id).and("oldState", scenariosCreated).and("now", time)); + if (count > 0) { + return scenariosCreated + 1; + } else { + Panache.getEntityManager().refresh(this); + } + } + + throw new IllegalStateException("Failed to allocate next scenario"); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java new file mode 100644 index 00000000..c10fcc64 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/ProjectAuthorization.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import io.quarkus.panache.common.Parameters; +import java.io.Serializable; +import java.util.List; +import java.util.Objects; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.MapsId; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import org.opendc.web.proto.user.ProjectRole; + +/** + * An authorization for some user to participate in a project. + */ +@Entity +@Table(name = "project_authorizations") +@NamedQueries({ + @NamedQuery( + name = "ProjectAuthorization.findByUser", + query = + """ + SELECT a + FROM ProjectAuthorization a + WHERE a.key.userId = :userId + """), +}) +public class ProjectAuthorization extends PanacheEntityBase { + /** + * The user identifier of the authorization. + */ + @EmbeddedId + public ProjectAuthorization.Key key; + + /** + * The project that the user is authorized to participate in. + */ + @ManyToOne(optional = false) + @MapsId("projectId") + @JoinColumn(name = "project_id", updatable = false, insertable = false, nullable = false) + public Project project; + + /** + * The role of the user in the project. + */ + @Column(nullable = false) + public ProjectRole role; + + /** + * Construct a {@link ProjectAuthorization} object. + */ + public ProjectAuthorization(Project project, String userId, ProjectRole role) { + this.key = new ProjectAuthorization.Key(project.id, userId); + this.project = project; + this.role = role; + } + + /** + * JPA constructor + */ + protected ProjectAuthorization() {} + + /** + * List all projects for the user with the specified userId. + * + * @param userId The identifier of the user that is requesting the list of projects. + * @return A list of projects that the user has received authorization for. + */ + public static List findByUser(String userId) { + return find("#ProjectAuthorization.findByUser", Parameters.with("userId", userId)) + .list(); + } + + /** + * Find the project with id for the user with the specified userId. + * + * @param userId The identifier of the user that is requesting the list of projects. + * @param id The unique identifier of the project. + * @return The project with the specified identifier or null if it does not exist or is not accessible + * to the user with the specified identifier. + */ + public static ProjectAuthorization findByUser(String userId, long id) { + return findById(new ProjectAuthorization.Key(id, userId)); + } + + /** + * Determine whether the authorization allows the user to edit the project. + */ + public boolean canEdit() { + return switch (role) { + case OWNER, EDITOR -> true; + case VIEWER -> false; + }; + } + + /** + * Determine whether the authorization allows the user to delete the project. + */ + public boolean canDelete() { + return role == ProjectRole.OWNER; + } + + /** + * Key for representing a {@link ProjectAuthorization} object. + */ + @Embeddable + public static class Key implements Serializable { + @Column(name = "project_id", nullable = false) + public long projectId; + + @Column(name = "user_id", nullable = false) + public String userId; + + public Key(long projectId, String userId) { + this.projectId = projectId; + this.userId = userId; + } + + protected Key() {} + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Key key = (Key) o; + return projectId == key.projectId && userId.equals(key.userId); + } + + @Override + public int hashCode() { + return Objects.hash(projectId, userId); + } + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java new file mode 100644 index 00000000..9381f9be --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Scenario.java @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.panache.common.Parameters; +import java.util.List; +import javax.persistence.CascadeType; +import javax.persistence.Column; +import javax.persistence.Embedded; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import org.hibernate.annotations.Type; +import org.opendc.web.proto.OperationalPhenomena; + +/** + * A single scenario to be explored by the simulator. + */ +@Entity +@Table( + name = "scenarios", + uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, + indexes = {@Index(name = "fn_scenarios_number", columnList = "project_id, number")}) +@NamedQueries({ + @NamedQuery(name = "Scenario.findByProject", query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId"), + @NamedQuery( + name = "Scenario.findByPortfolio", + query = + """ + SELECT s + FROM Scenario s + JOIN Portfolio p ON p.id = s.portfolio.id AND p.number = :number + WHERE s.project.id = :projectId + """), + @NamedQuery( + name = "Scenario.findOneByProject", + query = "SELECT s FROM Scenario s WHERE s.project.id = :projectId AND s.number = :number") +}) +public class Scenario extends PanacheEntity { + /** + * The {@link Project} to which this scenario belongs. + */ + @ManyToOne(optional = false) + @JoinColumn(name = "project_id", nullable = false) + public Project project; + + /** + * The {@link Portfolio} to which this scenario belongs. + */ + @ManyToOne(optional = false) + @JoinColumn(name = "portfolio_id", nullable = false) + public Portfolio portfolio; + + /** + * Unique number of the scenario for the project. + */ + @Column(nullable = false) + public int number; + + /** + * The name of the scenario. + */ + @Column(nullable = false, updatable = false) + public String name; + + /** + * Workload details of the scenario. + */ + @Embedded + public Workload workload; + + /** + * Topology details of the scenario. + */ + @ManyToOne(optional = false) + public Topology topology; + + /** + * Operational phenomena activated in the scenario. + */ + @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType") + @Column(columnDefinition = "jsonb", nullable = false, updatable = false) + public OperationalPhenomena phenomena; + + /** + * The name of the VM scheduler used in the scenario. + */ + @Column(name = "scheduler_name", nullable = false, updatable = false) + public String schedulerName; + + /** + * The {@link Job} associated with the scenario. + */ + @OneToOne(cascade = {CascadeType.ALL}) + public Job job; + + /** + * Construct a {@link Scenario} object. + */ + public Scenario( + Project project, + Portfolio portfolio, + int number, + String name, + Workload workload, + Topology topology, + OperationalPhenomena phenomena, + String schedulerName) { + this.project = project; + this.portfolio = portfolio; + this.number = number; + this.name = name; + this.workload = workload; + this.topology = topology; + this.phenomena = phenomena; + this.schedulerName = schedulerName; + } + + /** + * JPA constructor + */ + protected Scenario() {} + + /** + * Find all {@link Scenario}s that belong to the specified project + * + * @param projectId The unique identifier of the project. + * @return The list of scenarios that belong to the specified project. + */ + public static List findByProject(long projectId) { + return find("#Scenario.findByProject", Parameters.with("projectId", projectId)) + .list(); + } + + /** + * Find all {@link Scenario}s that belong to the specified portfolio. + * + * @param projectId The unique identifier of the project. + * @param number The number of the portfolio. + * @return The list of scenarios that belong to the specified project and portfolio.. + */ + public static List findByPortfolio(long projectId, int number) { + return find( + "#Scenario.findByPortfolio", + Parameters.with("projectId", projectId).and("number", number)) + .list(); + } + + /** + * Find the {@link Scenario} with the specified number belonging to the specified project. + * + * @param projectId The unique identifier of the project. + * @param number The number of the scenario. + * @return The scenario or null if it does not exist. + */ + public static Scenario findByProject(long projectId, int number) { + return find( + "#Scenario.findOneByProject", + Parameters.with("projectId", projectId).and("number", number)) + .firstResult(); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java new file mode 100644 index 00000000..6ec83f78 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Topology.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2022 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.panache.common.Parameters; +import java.time.Instant; +import java.util.List; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Index; +import javax.persistence.JoinColumn; +import javax.persistence.ManyToOne; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; +import javax.persistence.UniqueConstraint; +import org.hibernate.annotations.Type; +import org.opendc.web.proto.Room; + +/** + * A datacenter design in OpenDC. + */ +@Entity +@Table( + name = "topologies", + uniqueConstraints = {@UniqueConstraint(columnNames = {"project_id", "number"})}, + indexes = {@Index(name = "fn_topologies_number", columnList = "project_id, number")}) +@NamedQueries({ + @NamedQuery(name = "Topology.findByProject", query = "SELECT t FROM Topology t WHERE t.project.id = :projectId"), + @NamedQuery( + name = "Topology.findOneByProject", + query = "SELECT t FROM Topology t WHERE t.project.id = :projectId AND t.number = :number") +}) +public class Topology extends PanacheEntity { + /** + * The {@link Project} to which the topology belongs. + */ + @ManyToOne(optional = false) + @JoinColumn(name = "project_id", nullable = false) + public Project project; + + /** + * Unique number of the topology for the project. + */ + @Column(nullable = false) + public int number; + + /** + * The name of the topology. + */ + @Column(nullable = false) + public String name; + + /** + * The instant at which the topology was created. + */ + @Column(name = "created_at", nullable = false, updatable = false) + public Instant createdAt; + + /** + * The instant at which the topology was updated. + */ + @Column(name = "updated_at", nullable = false) + public Instant updatedAt; + + /** + * Datacenter design in JSON + */ + @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType") + @Column(columnDefinition = "jsonb", nullable = false) + public List rooms; + + /** + * Construct a {@link Topology} object. + */ + public Topology(Project project, int number, String name, Instant createdAt, List rooms) { + this.project = project; + this.number = number; + this.name = name; + this.createdAt = createdAt; + this.updatedAt = createdAt; + this.rooms = rooms; + } + + /** + * JPA constructor + */ + protected Topology() {} + + /** + * Find all [Topology]s that belong to [project][projectId]. + * + * @param projectId The unique identifier of the project. + * @return The list of topologies that belong to the specified project. + */ + public static List findByProject(long projectId) { + return find("#Topology.findByProject", Parameters.with("projectId", projectId)) + .list(); + } + + /** + * Find the [Topology] with the specified [number] belonging to [project][projectId]. + * + * @param projectId The unique identifier of the project. + * @param number The number of the topology. + * @return The topology or `null` if it does not exist. + */ + public static Topology findByProject(long projectId, int number) { + return find( + "#Topology.findOneByProject", + Parameters.with("projectId", projectId).and("number", number)) + .firstResult(); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java new file mode 100644 index 00000000..f73c8494 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Trace.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2021 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; + +/** + * A workload trace available for simulation. + */ +@Entity +public class Trace extends PanacheEntityBase { + /** + * The unique identifier of the trace. + */ + @Id + public String id; + + /** + * The name of the trace. + */ + @Column(nullable = false, updatable = false) + public String name; + + /** + * The type of trace. + */ + @Column(nullable = false, updatable = false) + public String type; + + /** + * Construct a {@link Trace}. + * + * @param id The unique identifier of the trace. + * @param name The name of the trace. + * @param type The type of trace. + */ + public Trace(String id, String name, String type) { + this.id = id; + this.name = name; + this.type = type; + } + + /** + * JPA constructor. + */ + protected Trace() {} +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java new file mode 100644 index 00000000..fda4302f --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/UserAccounting.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.model; + +import io.quarkus.hibernate.orm.panache.PanacheEntityBase; +import io.quarkus.panache.common.Parameters; +import java.time.LocalDate; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +/** + * Entity to track the number of simulation minutes used by a user. + */ +@Entity +@Table(name = "user_accounting") +@NamedQueries({ + @NamedQuery( + name = "UserAccounting.consumeBudget", + query = + """ + UPDATE UserAccounting a + SET a.simulationTime = a.simulationTime + :seconds + WHERE a.userId = :userId AND a.periodEnd = :periodEnd + """), + @NamedQuery( + name = "UserAccounting.resetBudget", + query = + """ + UPDATE UserAccounting a + SET a.periodEnd = :periodEnd, a.simulationTime = :seconds + WHERE a.userId = :userId AND a.periodEnd = :oldPeriodEnd + """) +}) +public class UserAccounting extends PanacheEntityBase { + /** + * User to which this object belongs. + */ + @Id + @Column(name = "user_id", nullable = false) + public String userId; + + /** + * The end of the accounting period. + */ + @Column(name = "period_end", nullable = false) + public LocalDate periodEnd; + + /** + * The number of simulation seconds to be used per accounting period. + */ + @Column(name = "simulation_time_budget", nullable = false) + public int simulationTimeBudget; + + /** + * The number of simulation seconds used in this period. This number should reset once the accounting period has + * been reached. + */ + @Column(name = "simulation_time", nullable = false) + public int simulationTime = 0; + + /** + * Construct a new {@link UserAccounting} object. + * + * @param userId The identifier of the user that this object belongs to. + * @param periodEnd The end of the accounting period. + * @param simulationTimeBudget The number of simulation seconds available per accounting period. + */ + public UserAccounting(String userId, LocalDate periodEnd, int simulationTimeBudget) { + this.userId = userId; + this.periodEnd = periodEnd; + this.simulationTimeBudget = simulationTimeBudget; + } + + /** + * JPA constructor. + */ + protected UserAccounting() {} + + /** + * Return the {@link UserAccounting} object associated with the specified user id. + */ + public static UserAccounting findByUser(String userId) { + return findById(userId); + } + + /** + * Create a new {@link UserAccounting} object and persist it to the database. + * + * @param userId The identifier of the user that this object belongs to. + * @param periodEnd The end of the accounting period. + * @param simulationTimeBudget The number of simulation seconds available per accounting period. + * @param simulationTime The initial simulation time that has been consumed. + */ + public static UserAccounting create( + String userId, LocalDate periodEnd, int simulationTimeBudget, int simulationTime) { + UserAccounting newAccounting = new UserAccounting(userId, periodEnd, simulationTimeBudget); + newAccounting.simulationTime = simulationTime; + newAccounting.persistAndFlush(); + return newAccounting; + } + + /** + * Atomically consume the budget for this {@link UserAccounting} object. + * + * @param seconds The number of seconds to consume from the user. + * @return true when the update succeeded, false when there was a conflict. + */ + public boolean consumeBudget(int seconds) { + long count = update( + "#UserAccounting.consumeBudget", + Parameters.with("userId", userId).and("periodEnd", periodEnd).and("seconds", seconds)); + return count > 0; + } + + /** + * Atomically reset the budget for this {@link UserAccounting} object. + * + * @param periodEnd The new end period for the budget. + * @param seconds The number of seconds that have already been consumed. + * @return true when the update succeeded`, false when there was a conflict. + */ + public boolean resetBudget(LocalDate periodEnd, int seconds) { + long count = update( + "#UserAccounting.resetBudget", + Parameters.with("userId", userId) + .and("oldPeriodEnd", this.periodEnd) + .and("periodEnd", periodEnd) + .and("seconds", seconds)); + return count > 0; + } + + /** + * Determine whether the user has any remaining simulation budget. + * + * @return true when the user still has budget left, false otherwise. + */ + public boolean hasSimulationBudget() { + var today = LocalDate.now(); + + // The accounting period must be over or there must be budget remaining. + return !today.isBefore(periodEnd) || simulationTimeBudget > simulationTime; + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Workload.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Workload.java new file mode 100644 index 00000000..129fb0c5 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/model/Workload.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.model; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import javax.persistence.ManyToOne; + +/** + * Specification of the workload for a {@link Scenario} + */ +@Embeddable +public class Workload { + /** + * The {@link Trace} that the workload runs. + */ + @ManyToOne(optional = false) + public Trace trace; + + /** + * The percentage of the trace that should be sampled. + */ + @Column(name = "sampling_fraction", nullable = false, updatable = false) + public double samplingFraction; + + /** + * Construct a {@link Workload} object. + * + * @param trace The {@link Trace} to run as workload. + * @param samplingFraction The percentage of the workload to sample. + */ + public Workload(Trace trace, double samplingFraction) { + this.trace = trace; + this.samplingFraction = samplingFraction; + } + + /** + * JPA constructor. + */ + protected Workload() {} +} 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 new file mode 100644 index 00000000..eb0982ec --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/JobService.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.service; + +import java.time.Instant; +import java.util.List; +import java.util.Map; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; + +/** + * Service for managing {@link Job}s. + */ +@ApplicationScoped +public final class JobService { + /** + * The service for managing the user accounting. + */ + private final UserAccountingService accountingService; + + /** + * Construct a {@link JobService} instance. + * + * @param accountingService The {@link UserAccountingService} instance to use. + */ + public JobService(UserAccountingService accountingService) { + this.accountingService = accountingService; + } + + /** + * Query the pending simulation jobs. + */ + public List listPending() { + return Job.findByState(JobState.PENDING).stream() + .map(JobService::toRunnerDto) + .toList(); + } + + /** + * Find a job by its identifier. + */ + public org.opendc.web.proto.runner.Job findById(long id) { + return toRunnerDto(Job.findById(id)); + } + + /** + * Atomically update the state of a {@link Job}. + * + * @param id The identifier of the job. + * @param newState The next state for the job. + * @param runtime The runtime of the job (in seconds). + * @param results The potential results of the job. + */ + public org.opendc.web.proto.runner.Job updateState( + long id, JobState newState, int runtime, Map results) { + Job entity = Job.findById(id); + if (entity == null) { + return null; + } + + JobState state = entity.state; + if (!isTransitionLegal(state, newState)) { + throw new IllegalArgumentException("Invalid transition from %s to %s".formatted(state, newState)); + } + + Instant now = Instant.now(); + JobState nextState = newState; + int consumedBudget = Math.min(1, runtime - entity.runtime); + + // Check whether the user still has any simulation budget left + if (accountingService.consumeSimulationBudget(entity.createdBy, consumedBudget) + && nextState == JobState.RUNNING) { + nextState = JobState.FAILED; // User has consumed all their budget; cancel the job + } + + if (!entity.updateAtomically(nextState, now, runtime, results)) { + throw new IllegalStateException("Conflicting update"); + } + + return toRunnerDto(entity); + } + + /** + * Determine whether the transition from [this] to [newState] is legal. + */ + public static boolean isTransitionLegal(JobState currentState, JobState newState) { + // Note that we always allow transitions from the state + return newState == currentState + || switch (currentState) { + case PENDING -> newState == JobState.CLAIMED; + case CLAIMED -> newState == JobState.RUNNING || newState == JobState.FAILED; + case RUNNING -> newState == JobState.FINISHED || newState == JobState.FAILED; + case FINISHED, FAILED -> false; + }; + } + + /** + * Convert a {@link Job} entity into a {@link org.opendc.web.proto.user.Job} DTO. + */ + public static org.opendc.web.proto.user.Job toUserDto(Job job) { + return new org.opendc.web.proto.user.Job(job.id, job.state, job.createdAt, job.updatedAt, job.results); + } + + /** + * Convert a {@link Job} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Job toRunnerDto(Job job) { + return new org.opendc.web.proto.runner.Job( + job.id, + ScenarioService.toRunnerDto(job.scenario), + job.state, + job.createdAt, + job.updatedAt, + job.runtime, + job.results); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java new file mode 100644 index 00000000..94da5195 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/PortfolioService.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.service; + +import java.time.Instant; +import java.util.List; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.ProjectAuthorization; + +/** + * Service for managing {@link Portfolio}s. + */ +@ApplicationScoped +public final class PortfolioService { + /** + * List all {@link Portfolio}s that belong a certain project. + */ + public List findByUser(String userId, long projectId) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return List.of(); + } + + return Portfolio.findByProject(projectId).stream() + .map((p) -> toUserDto(p, auth)) + .toList(); + } + + /** + * Find a {@link Portfolio} with the specified number belonging to projectId. + */ + public org.opendc.web.proto.user.Portfolio findByUser(String userId, long projectId, int number) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } + + Portfolio portfolio = Portfolio.findByProject(projectId, number); + + if (portfolio == null) { + return null; + } + + return toUserDto(portfolio, auth); + } + + /** + * Delete the portfolio with the specified number belonging to projectId. + */ + public org.opendc.web.proto.user.Portfolio delete(String userId, long projectId, int number) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } else if (!auth.canEdit()) { + throw new IllegalStateException("Not permitted to edit project"); + } + + Portfolio entity = Portfolio.findByProject(projectId, number); + if (entity == null) { + return null; + } + + entity.delete(); + return toUserDto(entity, auth); + } + + /** + * Construct a new {@link Portfolio} with the specified name. + */ + public org.opendc.web.proto.user.Portfolio create( + String userId, long projectId, org.opendc.web.proto.user.Portfolio.Create request) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } else if (!auth.canEdit()) { + throw new IllegalStateException("Not permitted to edit project"); + } + + var now = Instant.now(); + var project = auth.project; + int number = project.allocatePortfolio(now); + + Portfolio portfolio = new Portfolio(project, number, request.getName(), request.getTargets()); + + project.portfolios.add(portfolio); + portfolio.persist(); + + return toUserDto(portfolio, auth); + } + + /** + * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio} DTO. + */ + public static org.opendc.web.proto.user.Portfolio toUserDto(Portfolio portfolio, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Portfolio( + portfolio.id, + portfolio.number, + ProjectService.toUserDto(auth), + portfolio.name, + portfolio.targets, + portfolio.scenarios.stream().map(ScenarioService::toSummaryDto).toList()); + } + + /** + * Convert a {@link Portfolio} entity into a {@link org.opendc.web.proto.user.Portfolio.Summary} DTO. + */ + public static org.opendc.web.proto.user.Portfolio.Summary toSummaryDto(Portfolio portfolio) { + return new org.opendc.web.proto.user.Portfolio.Summary( + portfolio.id, portfolio.number, portfolio.name, portfolio.targets); + } + + /** + * Convert a {@link Portfolio} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Portfolio toRunnerDto(Portfolio portfolio) { + return new org.opendc.web.proto.runner.Portfolio( + portfolio.id, portfolio.number, portfolio.name, portfolio.targets); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java new file mode 100644 index 00000000..aeef664e --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ProjectService.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.service; + +import java.time.Instant; +import java.util.List; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.proto.user.ProjectRole; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; + +/** + * Service for managing {@link Project}s. + */ +@ApplicationScoped +public final class ProjectService { + /** + * List all projects for the user with the specified userId. + */ + public List findByUser(String userId) { + return ProjectAuthorization.findByUser(userId).stream() + .map(ProjectService::toUserDto) + .toList(); + } + + /** + * Obtain the project with the specified id for the user with the specified userId. + */ + public org.opendc.web.proto.user.Project findByUser(String userId, long id) { + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, id); + + if (auth == null) { + return null; + } + + return toUserDto(auth); + } + + /** + * Create a new {@link Project} for the user with the specified userId. + */ + public org.opendc.web.proto.user.Project create(String userId, String name) { + Instant now = Instant.now(); + Project entity = new Project(name, now); + entity.persist(); + + ProjectAuthorization authorization = new ProjectAuthorization(entity, userId, ProjectRole.OWNER); + + entity.authorizations.add(authorization); + authorization.persist(); + + return toUserDto(authorization); + } + + /** + * Delete a project by its identifier. + * + * @param userId The user that invokes the action. + * @param id The identifier of the project. + */ + public org.opendc.web.proto.user.Project delete(String userId, long id) { + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, id); + + if (auth == null) { + return null; + } + + if (!auth.canDelete()) { + throw new IllegalArgumentException("Not allowed to delete project"); + } + + auth.project.updatedAt = Instant.now(); + org.opendc.web.proto.user.Project project = toUserDto(auth); + auth.project.delete(); + return project; + } + + /** + * Convert a {@link ProjectAuthorization} entity into a {@link Project} DTO. + */ + public static org.opendc.web.proto.user.Project toUserDto(ProjectAuthorization auth) { + Project project = auth.project; + return new org.opendc.web.proto.user.Project( + project.id, project.name, project.createdAt, project.updatedAt, auth.role); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java new file mode 100644 index 00000000..bf5206af --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/ScenarioService.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.service; + +import java.time.Instant; +import java.util.List; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.proto.JobState; +import org.opendc.web.server.model.Job; +import org.opendc.web.server.model.Portfolio; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Scenario; +import org.opendc.web.server.model.Topology; +import org.opendc.web.server.model.Trace; +import org.opendc.web.server.model.Workload; + +/** + * Service for managing {@link Scenario}s. + */ +@ApplicationScoped +public final class ScenarioService { + /** + * The service for managing the user accounting. + */ + private final UserAccountingService accountingService; + + /** + * Construct a {@link ScenarioService} instance. + * + * @param accountingService The {@link UserAccountingService} instance to use. + */ + public ScenarioService(UserAccountingService accountingService) { + this.accountingService = accountingService; + } + + /** + * List all {@link Scenario}s that belong a certain portfolio. + */ + public List findAll(String userId, long projectId, int number) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return List.of(); + } + + return Scenario.findByPortfolio(projectId, number).stream() + .map((s) -> toUserDto(s, auth)) + .toList(); + } + + /** + * Obtain a {@link Scenario} by identifier. + */ + public org.opendc.web.proto.user.Scenario findOne(String userId, long projectId, int number) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } + + Scenario scenario = Scenario.findByProject(projectId, number); + + if (scenario == null) { + return null; + } + + return toUserDto(scenario, auth); + } + + /** + * Delete the specified scenario. + */ + public org.opendc.web.proto.user.Scenario delete(String userId, long projectId, int number) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } else if (!auth.canEdit()) { + throw new IllegalStateException("Not permitted to edit project"); + } + + Scenario entity = Scenario.findByProject(projectId, number); + if (entity == null) { + return null; + } + + entity.delete(); + return toUserDto(entity, auth); + } + + /** + * Construct a new {@link Scenario} with the specified data. + */ + public org.opendc.web.proto.user.Scenario create( + String userId, long projectId, int portfolioNumber, org.opendc.web.proto.user.Scenario.Create request) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } else if (!auth.canEdit()) { + throw new IllegalStateException("Not permitted to edit project"); + } + + Portfolio portfolio = Portfolio.findByProject(projectId, portfolioNumber); + + if (portfolio == null) { + return null; + } + + Topology topology = Topology.findByProject(projectId, (int) request.getTopology()); + if (topology == null) { + throw new IllegalArgumentException("Referred topology does not exist"); + } + + Trace trace = Trace.findById(request.getWorkload().getTrace()); + if (trace == null) { + throw new IllegalArgumentException("Referred trace does not exist"); + } + + var now = Instant.now(); + var project = auth.project; + int number = project.allocateScenario(now); + + Scenario scenario = new Scenario( + project, + portfolio, + number, + request.getName(), + new Workload(trace, request.getWorkload().getSamplingFraction()), + topology, + request.getPhenomena(), + request.getSchedulerName()); + Job job = new Job(scenario, userId, now, portfolio.targets.getRepeats()); + + // Fail the job if there is not enough budget for the simulation + if (!accountingService.hasSimulationBudget(userId)) { + job.state = JobState.FAILED; + } + + scenario.job = job; + portfolio.scenarios.add(scenario); + scenario.persist(); + + return toUserDto(scenario, auth); + } + + /** + * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario} DTO. + */ + public static org.opendc.web.proto.user.Scenario toUserDto(Scenario scenario, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Scenario( + scenario.id, + scenario.number, + ProjectService.toUserDto(auth), + PortfolioService.toSummaryDto(scenario.portfolio), + scenario.name, + toDto(scenario.workload), + TopologyService.toSummaryDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName, + JobService.toUserDto(scenario.job)); + } + + /** + * Convert a {@link Scenario} entity into a {@link org.opendc.web.proto.user.Scenario.Summary} DTO. + */ + public static org.opendc.web.proto.user.Scenario.Summary toSummaryDto(Scenario scenario) { + return new org.opendc.web.proto.user.Scenario.Summary( + scenario.id, + scenario.number, + scenario.name, + toDto(scenario.workload), + TopologyService.toSummaryDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName, + JobService.toUserDto(scenario.job)); + } + + /** + * Convert a {@link Scenario} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Scenario toRunnerDto(Scenario scenario) { + return new org.opendc.web.proto.runner.Scenario( + scenario.id, + scenario.number, + PortfolioService.toRunnerDto(scenario.portfolio), + scenario.name, + toDto(scenario.workload), + TopologyService.toRunnerDto(scenario.topology), + scenario.phenomena, + scenario.schedulerName); + } + + /** + * Convert a {@link Workload} entity into a DTO. + */ + public static org.opendc.web.proto.Workload toDto(Workload workload) { + return new org.opendc.web.proto.Workload(TraceService.toUserDto(workload.trace), workload.samplingFraction); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java new file mode 100644 index 00000000..1961995f --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TopologyService.java @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.service; + +import java.time.Instant; +import java.util.List; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.server.model.Project; +import org.opendc.web.server.model.ProjectAuthorization; +import org.opendc.web.server.model.Topology; + +/** + * Service for managing {@link Topology}s. + */ +@ApplicationScoped +public final class TopologyService { + /** + * List all {@link Topology}s that belong a certain project. + */ + public List findAll(String userId, long projectId) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return List.of(); + } + + return Topology.findByProject(projectId).stream() + .map((t) -> toUserDto(t, auth)) + .toList(); + } + + /** + * Find the {@link Topology} with the specified number belonging to projectId. + */ + public org.opendc.web.proto.user.Topology findOne(String userId, long projectId, int number) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } + + Topology topology = Topology.findByProject(projectId, number); + + if (topology == null) { + return null; + } + + return toUserDto(topology, auth); + } + + /** + * Delete the {@link Topology} with the specified number belonging to projectId + */ + public org.opendc.web.proto.user.Topology delete(String userId, long projectId, int number) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } else if (!auth.canEdit()) { + throw new IllegalStateException("Not permitted to edit project"); + } + + Topology entity = Topology.findByProject(projectId, number); + + if (entity == null) { + return null; + } + + entity.updatedAt = Instant.now(); + entity.delete(); + return toUserDto(entity, auth); + } + + /** + * Update a {@link Topology} with the specified number belonging to projectId. + */ + public org.opendc.web.proto.user.Topology update( + String userId, long projectId, int number, org.opendc.web.proto.user.Topology.Update request) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } else if (!auth.canEdit()) { + throw new IllegalStateException("Not permitted to edit project"); + } + + Topology entity = Topology.findByProject(projectId, number); + + if (entity == null) { + return null; + } + + entity.updatedAt = Instant.now(); + entity.rooms = request.getRooms(); + + return toUserDto(entity, auth); + } + + /** + * Construct a new {@link Topology} with the specified name. + */ + public org.opendc.web.proto.user.Topology create( + String userId, long projectId, org.opendc.web.proto.user.Topology.Create request) { + // User must have access to project + ProjectAuthorization auth = ProjectAuthorization.findByUser(userId, projectId); + + if (auth == null) { + return null; + } else if (!auth.canEdit()) { + throw new IllegalStateException("Not permitted to edit project"); + } + + Instant now = Instant.now(); + Project project = auth.project; + int number = project.allocateTopology(now); + + Topology topology = new Topology(project, number, request.getName(), now, request.getRooms()); + + project.topologies.add(topology); + topology.persist(); + + return toUserDto(topology, auth); + } + + /** + * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology} DTO. + */ + public static org.opendc.web.proto.user.Topology toUserDto(Topology topology, ProjectAuthorization auth) { + return new org.opendc.web.proto.user.Topology( + topology.id, + topology.number, + ProjectService.toUserDto(auth), + topology.name, + topology.rooms, + topology.createdAt, + topology.updatedAt); + } + + /** + * Convert a {@link Topology} entity into a {@link org.opendc.web.proto.user.Topology.Summary} DTO. + */ + public static org.opendc.web.proto.user.Topology.Summary toSummaryDto(Topology topology) { + return new org.opendc.web.proto.user.Topology.Summary( + topology.id, topology.number, topology.name, topology.createdAt, topology.updatedAt); + } + + /** + * Convert a {@link Topology} into a runner-facing DTO. + */ + public static org.opendc.web.proto.runner.Topology toRunnerDto(Topology topology) { + return new org.opendc.web.proto.runner.Topology( + topology.id, topology.number, topology.name, topology.rooms, topology.createdAt, topology.updatedAt); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TraceService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TraceService.java new file mode 100644 index 00000000..94b8340b --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/TraceService.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.service; + +import java.util.List; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.server.model.Trace; + +/** + * Service for managing {@link Trace}s. + */ +@ApplicationScoped +public final class TraceService { + /** + * Obtain all available workload traces. + */ + public List findAll() { + List entities = Trace.listAll(); + return entities.stream().map(TraceService::toUserDto).toList(); + } + + /** + * Obtain a workload trace by identifier. + */ + public org.opendc.web.proto.Trace findById(String id) { + return toUserDto(Trace.findById(id)); + } + + /** + * Convert a {@link Trace] entity into a {@link org.opendc.web.proto.Trace} DTO. + */ + public static org.opendc.web.proto.Trace toUserDto(Trace trace) { + return new org.opendc.web.proto.Trace(trace.id, trace.name, trace.type); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java new file mode 100644 index 00000000..e5003cb4 --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserAccountingService.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.service; + +import java.time.Duration; +import java.time.LocalDate; +import java.time.temporal.TemporalAdjusters; +import javax.enterprise.context.ApplicationScoped; +import javax.persistence.EntityExistsException; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.opendc.web.server.model.UserAccounting; + +/** + * Service to track the simulation budget of users. + */ +@ApplicationScoped +public final class UserAccountingService { + /** + * The default simulation budget for new users. + */ + private final Duration simulationBudget; + + /** + * Construct a {@link UserAccountingService} instance. + * + * @param simulationBudget The default simulation budget for new users. + */ + public UserAccountingService( + @ConfigProperty(name = "opendc.accounting.simulation-budget", defaultValue = "2000m") + Duration simulationBudget) { + this.simulationBudget = simulationBudget; + } + + /** + * Return the {@link org.opendc.web.proto.user.UserAccounting} object for the user with the + * specified userId. If the object does not exist in the database, a default value is constructed. + */ + public org.opendc.web.proto.user.UserAccounting getAccounting(String userId) { + UserAccounting accounting = UserAccounting.findByUser(userId); + if (accounting != null) { + return new org.opendc.web.proto.user.UserAccounting( + accounting.periodEnd, accounting.simulationTime, accounting.simulationTimeBudget); + } + + return new org.opendc.web.proto.user.UserAccounting( + getNextAccountingPeriod(LocalDate.now()), 0, (int) simulationBudget.toSeconds()); + } + + /** + * Determine whether the user with userId has any remaining simulation budget. + * + * @param userId The unique identifier of the user. + * @return true when the user still has budget left, false otherwise. + */ + public boolean hasSimulationBudget(String userId) { + UserAccounting accounting = UserAccounting.findByUser(userId); + if (accounting == null) { + return true; + } + return accounting.hasSimulationBudget(); + } + + /** + * Consume seconds from the simulation budget of the user with userId. + * + * @param userId The unique identifier of the user. + * @param seconds The seconds to consume from the simulation budget. + * @return true if the user has consumed his full budget or false if there is still budget + * remaining. + */ + public boolean consumeSimulationBudget(String userId, int seconds) { + LocalDate today = LocalDate.now(); + LocalDate nextAccountingPeriod = getNextAccountingPeriod(today); + + // We need to be careful to prevent conflicts in case of concurrency + // 1. First, we try to create the accounting object if it does not exist yet. This may fail if another instance + // creates the object concurrently. + // 2. Second, we check if the budget needs to be reset and try this atomically. + // 3. Finally, we atomically consume the budget from the object + // This is repeated three times in case there is a conflict + for (int i = 0; i < 3; i++) { + UserAccounting accounting = UserAccounting.findByUser(userId); + + if (accounting == null) { + try { + UserAccounting newAccounting = UserAccounting.create( + userId, nextAccountingPeriod, (int) simulationBudget.toSeconds(), seconds); + return !newAccounting.hasSimulationBudget(); + } catch (EntityExistsException e) { + // Conflict due to concurrency; retry + } + } else { + boolean success; + + if (!today.isBefore(accounting.periodEnd)) { + success = accounting.resetBudget(nextAccountingPeriod, seconds); + } else { + success = accounting.consumeBudget(seconds); + } + + if (success) { + return !accounting.hasSimulationBudget(); + } + } + } + + throw new IllegalStateException("Failed to allocate consume budget due to conflict"); + } + + /** + * Helper method to find next accounting period. + */ + private static LocalDate getNextAccountingPeriod(LocalDate today) { + return today.with(TemporalAdjusters.firstDayOfNextMonth()); + } +} diff --git a/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserService.java b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserService.java new file mode 100644 index 00000000..b46b799b --- /dev/null +++ b/opendc-web/opendc-web-server/src/main/java/org/opendc/web/server/service/UserService.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 AtLarge Research + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.opendc.web.server.service; + +import io.quarkus.security.identity.SecurityIdentity; +import javax.enterprise.context.ApplicationScoped; +import org.opendc.web.proto.user.User; +import org.opendc.web.proto.user.UserAccounting; + +/** + * Service for managing {@link User}s. + */ +@ApplicationScoped +public final class UserService { + /** + * The service for managing the user accounting. + */ + private final UserAccountingService accountingService; + + /** + * Construct a {@link UserService} instance. + * + * @param accountingService The {@link UserAccountingService} instance to use. + */ + public UserService(UserAccountingService accountingService) { + this.accountingService = accountingService; + } + + /** + * Obtain the {@link User} object for the specified identity. + */ + public User getUser(SecurityIdentity identity) { + String userId = identity.getPrincipal().getName(); + UserAccounting accounting = accountingService.getAccounting(userId); + + return new User(userId, accounting); + } +} 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 ed1f7bf1..84ebd6e4 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 @@ -47,7 +47,7 @@ public class QuarkusJobManager implements JobManager { @Nullable @Override public Job findNext() { - var pending = jobService.queryPending(); + var pending = jobService.listPending(); if (pending.isEmpty()) { return null; } -- cgit v1.2.3