feat: persist module training attempts and actions
parent
0f1794b1c7
commit
ae8ce2819c
@ -0,0 +1,8 @@
|
|||||||
|
package com.yau.digitalrmb.training.attempt.domain;
|
||||||
|
|
||||||
|
public enum ActionStatus {
|
||||||
|
RUNNING,
|
||||||
|
COMPLETED,
|
||||||
|
FAILED,
|
||||||
|
ROLLED_BACK
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package com.yau.digitalrmb.training.attempt.domain;
|
||||||
|
|
||||||
|
public enum AttemptStatus {
|
||||||
|
IN_PROGRESS,
|
||||||
|
SUBMITTED,
|
||||||
|
COMPLETED,
|
||||||
|
CANCELLED
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
package com.yau.digitalrmb.training.attempt.domain;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class ExperimentAction {
|
||||||
|
private final UUID id;
|
||||||
|
private final UUID attemptId;
|
||||||
|
private final String stepCode;
|
||||||
|
private final String actionCode;
|
||||||
|
private ActionStatus status;
|
||||||
|
private String requestFingerprint;
|
||||||
|
private String outputJson;
|
||||||
|
private String errorCode;
|
||||||
|
private String errorMessage;
|
||||||
|
private final Instant startedAt;
|
||||||
|
private Instant completedAt;
|
||||||
|
private Instant updatedAt;
|
||||||
|
private long version;
|
||||||
|
|
||||||
|
public ExperimentAction(UUID id, UUID attemptId, String stepCode, String actionCode, ActionStatus status,
|
||||||
|
String requestFingerprint, String outputJson, String errorCode, String errorMessage,
|
||||||
|
Instant startedAt, Instant completedAt, Instant updatedAt, long version) {
|
||||||
|
this.id = id;
|
||||||
|
this.attemptId = attemptId;
|
||||||
|
this.stepCode = stepCode;
|
||||||
|
this.actionCode = actionCode;
|
||||||
|
this.status = status;
|
||||||
|
this.requestFingerprint = requestFingerprint;
|
||||||
|
this.outputJson = outputJson;
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
this.startedAt = startedAt;
|
||||||
|
this.completedAt = completedAt;
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
this.version = version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExperimentAction running(UUID id, UUID attemptId, String stepCode,
|
||||||
|
String actionCode, String requestFingerprint) {
|
||||||
|
Instant now = Instant.now();
|
||||||
|
return new ExperimentAction(id, attemptId, stepCode, actionCode, ActionStatus.RUNNING,
|
||||||
|
requestFingerprint, null, null, null, now, null, now, 0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void complete(ActionStatus terminalStatus, String json, Instant now) {
|
||||||
|
if (terminalStatus != ActionStatus.COMPLETED && terminalStatus != ActionStatus.ROLLED_BACK) {
|
||||||
|
throw new IllegalArgumentException("动作终态不合法");
|
||||||
|
}
|
||||||
|
status = terminalStatus;
|
||||||
|
outputJson = json;
|
||||||
|
errorCode = null;
|
||||||
|
errorMessage = null;
|
||||||
|
completedAt = now;
|
||||||
|
updatedAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void fail(String code, String message, Instant now) {
|
||||||
|
status = ActionStatus.FAILED;
|
||||||
|
errorCode = code;
|
||||||
|
errorMessage = message;
|
||||||
|
completedAt = now;
|
||||||
|
updatedAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void restart(String fingerprint, Instant now) {
|
||||||
|
if (status != ActionStatus.FAILED && status != ActionStatus.ROLLED_BACK) {
|
||||||
|
throw new IllegalStateException("只有失败或已补偿动作可以重试");
|
||||||
|
}
|
||||||
|
status = ActionStatus.RUNNING;
|
||||||
|
requestFingerprint = fingerprint;
|
||||||
|
outputJson = null;
|
||||||
|
errorCode = null;
|
||||||
|
errorMessage = null;
|
||||||
|
completedAt = null;
|
||||||
|
updatedAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markPersisted() { version++; }
|
||||||
|
|
||||||
|
public UUID getId() { return id; }
|
||||||
|
public UUID getAttemptId() { return attemptId; }
|
||||||
|
public String getStepCode() { return stepCode; }
|
||||||
|
public String getActionCode() { return actionCode; }
|
||||||
|
public ActionStatus getStatus() { return status; }
|
||||||
|
public String getRequestFingerprint() { return requestFingerprint; }
|
||||||
|
public String getOutputJson() { return outputJson; }
|
||||||
|
public String getErrorCode() { return errorCode; }
|
||||||
|
public String getErrorMessage() { return errorMessage; }
|
||||||
|
public Instant getStartedAt() { return startedAt; }
|
||||||
|
public Instant getCompletedAt() { return completedAt; }
|
||||||
|
public Instant getUpdatedAt() { return updatedAt; }
|
||||||
|
public long getVersion() { return version; }
|
||||||
|
}
|
||||||
@ -0,0 +1,92 @@
|
|||||||
|
package com.yau.digitalrmb.training.attempt.domain;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public final class ExperimentAttempt {
|
||||||
|
private final UUID id;
|
||||||
|
private final ExperimentModule module;
|
||||||
|
private final ExperimentSubject subject;
|
||||||
|
private final int attemptNo;
|
||||||
|
private AttemptStatus status;
|
||||||
|
private String currentStepCode;
|
||||||
|
private String currentActionCode;
|
||||||
|
private String businessId;
|
||||||
|
private long version;
|
||||||
|
private final Instant createdAt;
|
||||||
|
private Instant updatedAt;
|
||||||
|
private Instant completedAt;
|
||||||
|
|
||||||
|
public ExperimentAttempt(UUID id, ExperimentModule module, ExperimentSubject subject, int attemptNo,
|
||||||
|
AttemptStatus status, String currentStepCode, String currentActionCode,
|
||||||
|
String businessId, long version, Instant createdAt, Instant updatedAt,
|
||||||
|
Instant completedAt) {
|
||||||
|
this.id = id;
|
||||||
|
this.module = module;
|
||||||
|
this.subject = subject;
|
||||||
|
this.attemptNo = attemptNo;
|
||||||
|
this.status = status;
|
||||||
|
this.currentStepCode = currentStepCode;
|
||||||
|
this.currentActionCode = currentActionCode;
|
||||||
|
this.businessId = businessId;
|
||||||
|
this.version = version;
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
this.updatedAt = updatedAt;
|
||||||
|
this.completedAt = completedAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ExperimentAttempt start(ExperimentModule module, ExperimentSubject subject, int attemptNo, Instant now) {
|
||||||
|
return new ExperimentAttempt(UUID.randomUUID(), module, subject, attemptNo, AttemptStatus.IN_PROGRESS,
|
||||||
|
"01", null, null, 0L, now, now, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void moveTo(String stepCode, String actionCode, Instant now) {
|
||||||
|
requireActive();
|
||||||
|
currentStepCode = stepCode;
|
||||||
|
currentActionCode = actionCode;
|
||||||
|
updatedAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bindBusinessId(String value, Instant now) {
|
||||||
|
requireActive();
|
||||||
|
if (businessId != null && value != null && !businessId.equals(value)) {
|
||||||
|
throw new IllegalStateException("实验尝试已经绑定其他业务记录");
|
||||||
|
}
|
||||||
|
businessId = value;
|
||||||
|
updatedAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void complete(Instant now) {
|
||||||
|
requireActive();
|
||||||
|
status = AttemptStatus.COMPLETED;
|
||||||
|
completedAt = now;
|
||||||
|
updatedAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel() {
|
||||||
|
requireActive();
|
||||||
|
status = AttemptStatus.CANCELLED;
|
||||||
|
updatedAt = Instant.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void markPersisted() { version++; }
|
||||||
|
|
||||||
|
private void requireActive() {
|
||||||
|
if (status != AttemptStatus.IN_PROGRESS && status != AttemptStatus.SUBMITTED) {
|
||||||
|
throw new IllegalStateException("实验尝试已经结束");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public UUID getId() { return id; }
|
||||||
|
public ExperimentModule getModule() { return module; }
|
||||||
|
public ExperimentSubject getSubject() { return subject; }
|
||||||
|
public int getAttemptNo() { return attemptNo; }
|
||||||
|
public AttemptStatus getStatus() { return status; }
|
||||||
|
public String getCurrentStepCode() { return currentStepCode; }
|
||||||
|
public String getCurrentActionCode() { return currentActionCode; }
|
||||||
|
public String getBusinessId() { return businessId; }
|
||||||
|
public long getVersion() { return version; }
|
||||||
|
public Instant getCreatedAt() { return createdAt; }
|
||||||
|
public Instant getUpdatedAt() { return updatedAt; }
|
||||||
|
public Instant getCompletedAt() { return completedAt; }
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
package com.yau.digitalrmb.training.attempt.domain;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface ExperimentAttemptRepository {
|
||||||
|
ExperimentAttempt create(ExperimentModule module, ExperimentSubject subject);
|
||||||
|
Optional<ExperimentAttempt> findCurrent(ExperimentModule module, ExperimentSubject subject);
|
||||||
|
Optional<ExperimentAttempt> findById(UUID id, ExperimentSubject subject);
|
||||||
|
Optional<ExperimentAttempt> findByIdForUpdate(UUID id, ExperimentSubject subject);
|
||||||
|
void save(ExperimentAttempt attempt);
|
||||||
|
void insertAction(ExperimentAction action);
|
||||||
|
void saveAction(ExperimentAction action);
|
||||||
|
Optional<ExperimentAction> findActionForUpdate(UUID attemptId, String stepCode, String actionCode);
|
||||||
|
List<ExperimentAction> findActions(UUID attemptId);
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
package com.yau.digitalrmb.training.attempt.domain;
|
||||||
|
|
||||||
|
public enum ExperimentModule {
|
||||||
|
ISSUANCE,
|
||||||
|
EXCHANGE,
|
||||||
|
PAYMENT
|
||||||
|
}
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
package com.yau.digitalrmb.training.attempt.domain;
|
||||||
|
|
||||||
|
public final class ExperimentSubject {
|
||||||
|
private final String userId;
|
||||||
|
private final long schoolId;
|
||||||
|
private final long classId;
|
||||||
|
private final String organizationId;
|
||||||
|
|
||||||
|
public ExperimentSubject(String userId, long schoolId, long classId, String organizationId) {
|
||||||
|
if (userId == null || userId.trim().isEmpty()) throw new IllegalArgumentException("用户ID不能为空");
|
||||||
|
this.userId = userId.trim();
|
||||||
|
this.schoolId = schoolId;
|
||||||
|
this.classId = classId;
|
||||||
|
this.organizationId = organizationId == null ? "" : organizationId.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUserId() { return userId; }
|
||||||
|
public long getSchoolId() { return schoolId; }
|
||||||
|
public long getClassId() { return classId; }
|
||||||
|
public String getOrganizationId() { return organizationId; }
|
||||||
|
}
|
||||||
@ -0,0 +1,168 @@
|
|||||||
|
package com.yau.digitalrmb.training.attempt.infrastructure;
|
||||||
|
|
||||||
|
import com.yau.digitalrmb.training.attempt.domain.ActionStatus;
|
||||||
|
import com.yau.digitalrmb.training.attempt.domain.AttemptStatus;
|
||||||
|
import com.yau.digitalrmb.training.attempt.domain.ExperimentAction;
|
||||||
|
import com.yau.digitalrmb.training.attempt.domain.ExperimentAttempt;
|
||||||
|
import com.yau.digitalrmb.training.attempt.domain.ExperimentAttemptRepository;
|
||||||
|
import com.yau.digitalrmb.training.attempt.domain.ExperimentModule;
|
||||||
|
import com.yau.digitalrmb.training.attempt.domain.ExperimentSubject;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.jdbc.core.RowMapper;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public class JdbcExperimentAttemptRepository implements ExperimentAttemptRepository {
|
||||||
|
private final JdbcTemplate jdbc;
|
||||||
|
|
||||||
|
public JdbcExperimentAttemptRepository(JdbcTemplate jdbc) {
|
||||||
|
this.jdbc = jdbc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional
|
||||||
|
public ExperimentAttempt create(ExperimentModule module, ExperimentSubject subject) {
|
||||||
|
Integer latest = jdbc.queryForObject("SELECT COALESCE(MAX(attempt_no),0) FROM training_experiment_attempt "
|
||||||
|
+ "WHERE module_code=? AND user_id=? AND school_id=? AND class_id=? "
|
||||||
|
+ "AND organization_id=?",
|
||||||
|
Integer.class, module.name(), subject.getUserId(), subject.getSchoolId(), subject.getClassId(),
|
||||||
|
subject.getOrganizationId());
|
||||||
|
ExperimentAttempt attempt = ExperimentAttempt.start(module, subject, (latest == null ? 0 : latest) + 1,
|
||||||
|
Instant.now());
|
||||||
|
jdbc.update("INSERT INTO training_experiment_attempt (id,module_code,user_id,school_id,class_id,"
|
||||||
|
+ "organization_id,attempt_no,status,current_step_code,current_action_code,business_id,version,"
|
||||||
|
+ "created_at,updated_at,completed_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
|
attempt.getId().toString(), module.name(), subject.getUserId(), subject.getSchoolId(),
|
||||||
|
subject.getClassId(), subject.getOrganizationId(), attempt.getAttemptNo(), attempt.getStatus().name(),
|
||||||
|
attempt.getCurrentStepCode(), attempt.getCurrentActionCode(), attempt.getBusinessId(),
|
||||||
|
attempt.getVersion(), timestamp(attempt.getCreatedAt()), timestamp(attempt.getUpdatedAt()), null);
|
||||||
|
return attempt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ExperimentAttempt> findCurrent(ExperimentModule module, ExperimentSubject subject) {
|
||||||
|
List<ExperimentAttempt> values = jdbc.query("SELECT * FROM training_experiment_attempt WHERE module_code=? "
|
||||||
|
+ "AND user_id=? AND school_id=? AND class_id=? "
|
||||||
|
+ "AND organization_id=? "
|
||||||
|
+ "AND status IN ('IN_PROGRESS','SUBMITTED') ORDER BY attempt_no DESC LIMIT 1",
|
||||||
|
attemptMapper(), module.name(), subject.getUserId(), subject.getSchoolId(), subject.getClassId(),
|
||||||
|
subject.getOrganizationId());
|
||||||
|
return optional(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ExperimentAttempt> findById(UUID id, ExperimentSubject subject) {
|
||||||
|
return findById(id, subject, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ExperimentAttempt> findByIdForUpdate(UUID id, ExperimentSubject subject) {
|
||||||
|
return findById(id, subject, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<ExperimentAttempt> findById(UUID id, ExperimentSubject subject, boolean forUpdate) {
|
||||||
|
List<ExperimentAttempt> values = jdbc.query("SELECT * FROM training_experiment_attempt WHERE id=? "
|
||||||
|
+ "AND user_id=? AND school_id=? AND class_id=? "
|
||||||
|
+ "AND organization_id=?"
|
||||||
|
+ (forUpdate ? " FOR UPDATE" : ""),
|
||||||
|
attemptMapper(), id.toString(), subject.getUserId(), subject.getSchoolId(), subject.getClassId(),
|
||||||
|
subject.getOrganizationId());
|
||||||
|
return optional(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void save(ExperimentAttempt attempt) {
|
||||||
|
int updated = jdbc.update("UPDATE training_experiment_attempt SET status=?,current_step_code=?,"
|
||||||
|
+ "current_action_code=?,business_id=?,version=version+1,updated_at=?,completed_at=? "
|
||||||
|
+ "WHERE id=? AND version=?",
|
||||||
|
attempt.getStatus().name(), attempt.getCurrentStepCode(), attempt.getCurrentActionCode(),
|
||||||
|
attempt.getBusinessId(), timestamp(attempt.getUpdatedAt()), timestamp(attempt.getCompletedAt()),
|
||||||
|
attempt.getId().toString(), attempt.getVersion());
|
||||||
|
if (updated != 1) throw new IllegalStateException("实验尝试状态已被其他请求修改");
|
||||||
|
attempt.markPersisted();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertAction(ExperimentAction action) {
|
||||||
|
jdbc.update("INSERT INTO training_experiment_action (id,attempt_id,step_code,action_code,status,"
|
||||||
|
+ "request_fingerprint,output_json,error_code,error_message,started_at,completed_at,updated_at,version) "
|
||||||
|
+ "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||||
|
action.getId().toString(), action.getAttemptId().toString(), action.getStepCode(), action.getActionCode(),
|
||||||
|
action.getStatus().name(), action.getRequestFingerprint(), action.getOutputJson(), action.getErrorCode(),
|
||||||
|
action.getErrorMessage(), timestamp(action.getStartedAt()), timestamp(action.getCompletedAt()),
|
||||||
|
timestamp(action.getUpdatedAt()), action.getVersion());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveAction(ExperimentAction action) {
|
||||||
|
int updated = jdbc.update("UPDATE training_experiment_action SET status=?,request_fingerprint=?,output_json=?,"
|
||||||
|
+ "error_code=?,error_message=?,completed_at=?,updated_at=?,version=version+1 "
|
||||||
|
+ "WHERE id=? AND version=?",
|
||||||
|
action.getStatus().name(), action.getRequestFingerprint(), action.getOutputJson(), action.getErrorCode(),
|
||||||
|
action.getErrorMessage(), timestamp(action.getCompletedAt()), timestamp(action.getUpdatedAt()),
|
||||||
|
action.getId().toString(), action.getVersion());
|
||||||
|
if (updated != 1) throw new IllegalStateException("实验动作状态已被其他请求修改");
|
||||||
|
action.markPersisted();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<ExperimentAction> findActionForUpdate(UUID attemptId, String stepCode, String actionCode) {
|
||||||
|
List<ExperimentAction> values = jdbc.query("SELECT * FROM training_experiment_action WHERE attempt_id=? "
|
||||||
|
+ "AND step_code=? AND action_code=? FOR UPDATE", actionMapper(),
|
||||||
|
attemptId.toString(), stepCode, actionCode);
|
||||||
|
return optional(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ExperimentAction> findActions(UUID attemptId) {
|
||||||
|
return jdbc.query("SELECT * FROM training_experiment_action WHERE attempt_id=? "
|
||||||
|
+ "ORDER BY started_at,id", actionMapper(), attemptId.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private RowMapper<ExperimentAttempt> attemptMapper() {
|
||||||
|
return new RowMapper<ExperimentAttempt>() {
|
||||||
|
@Override
|
||||||
|
public ExperimentAttempt mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||||
|
ExperimentSubject subject = new ExperimentSubject(rs.getString("user_id"), rs.getLong("school_id"),
|
||||||
|
rs.getLong("class_id"), rs.getString("organization_id"));
|
||||||
|
return new ExperimentAttempt(UUID.fromString(rs.getString("id")),
|
||||||
|
ExperimentModule.valueOf(rs.getString("module_code")), subject, rs.getInt("attempt_no"),
|
||||||
|
AttemptStatus.valueOf(rs.getString("status")), rs.getString("current_step_code"),
|
||||||
|
rs.getString("current_action_code"), rs.getString("business_id"), rs.getLong("version"),
|
||||||
|
instant(rs.getTimestamp("created_at")), instant(rs.getTimestamp("updated_at")),
|
||||||
|
instant(rs.getTimestamp("completed_at")));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private RowMapper<ExperimentAction> actionMapper() {
|
||||||
|
return new RowMapper<ExperimentAction>() {
|
||||||
|
@Override
|
||||||
|
public ExperimentAction mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||||
|
return new ExperimentAction(UUID.fromString(rs.getString("id")),
|
||||||
|
UUID.fromString(rs.getString("attempt_id")), rs.getString("step_code"),
|
||||||
|
rs.getString("action_code"), ActionStatus.valueOf(rs.getString("status")),
|
||||||
|
rs.getString("request_fingerprint"), rs.getString("output_json"),
|
||||||
|
rs.getString("error_code"), rs.getString("error_message"),
|
||||||
|
instant(rs.getTimestamp("started_at")), instant(rs.getTimestamp("completed_at")),
|
||||||
|
instant(rs.getTimestamp("updated_at")), rs.getLong("version"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static <T> Optional<T> optional(List<T> values) {
|
||||||
|
return values.isEmpty() ? Optional.<T>empty() : Optional.of(values.get(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Timestamp timestamp(Instant value) { return value == null ? null : Timestamp.from(value); }
|
||||||
|
private static Instant instant(Timestamp value) { return value == null ? null : value.toInstant(); }
|
||||||
|
}
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS training_experiment_attempt (
|
||||||
|
id CHAR(36) PRIMARY KEY,
|
||||||
|
module_code VARCHAR(32) NOT NULL,
|
||||||
|
user_id VARCHAR(36) NOT NULL,
|
||||||
|
school_id BIGINT NOT NULL,
|
||||||
|
class_id BIGINT NOT NULL,
|
||||||
|
organization_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
|
attempt_no INT NOT NULL,
|
||||||
|
status VARCHAR(32) NOT NULL,
|
||||||
|
current_step_code VARCHAR(16) NULL,
|
||||||
|
current_action_code VARCHAR(64) NULL,
|
||||||
|
business_id VARCHAR(96) NULL,
|
||||||
|
version BIGINT NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMP NOT NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL,
|
||||||
|
completed_at TIMESTAMP NULL,
|
||||||
|
CONSTRAINT uk_experiment_attempt_no
|
||||||
|
UNIQUE (module_code, user_id, school_id, class_id, organization_id, attempt_no),
|
||||||
|
INDEX idx_experiment_attempt_current
|
||||||
|
(module_code, user_id, school_id, class_id, status, attempt_no)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS training_experiment_action (
|
||||||
|
id CHAR(36) PRIMARY KEY,
|
||||||
|
attempt_id CHAR(36) NOT NULL,
|
||||||
|
step_code VARCHAR(16) NOT NULL,
|
||||||
|
action_code VARCHAR(64) NOT NULL,
|
||||||
|
status VARCHAR(32) NOT NULL,
|
||||||
|
request_fingerprint CHAR(64) NOT NULL,
|
||||||
|
output_json TEXT NULL,
|
||||||
|
error_code VARCHAR(32) NULL,
|
||||||
|
error_message VARCHAR(512) NULL,
|
||||||
|
started_at TIMESTAMP NOT NULL,
|
||||||
|
completed_at TIMESTAMP NULL,
|
||||||
|
updated_at TIMESTAMP NOT NULL,
|
||||||
|
version BIGINT NOT NULL DEFAULT 0,
|
||||||
|
CONSTRAINT uk_experiment_action UNIQUE (attempt_id, step_code, action_code),
|
||||||
|
INDEX idx_experiment_action_attempt (attempt_id, started_at),
|
||||||
|
CONSTRAINT fk_experiment_action_attempt
|
||||||
|
FOREIGN KEY (attempt_id) REFERENCES training_experiment_attempt(id)
|
||||||
|
);
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
package com.yau.digitalrmb.training.attempt.infrastructure;
|
||||||
|
|
||||||
|
import com.yau.digitalrmb.training.attempt.domain.ActionStatus;
|
||||||
|
import com.yau.digitalrmb.training.attempt.domain.ExperimentAction;
|
||||||
|
import com.yau.digitalrmb.training.attempt.domain.ExperimentAttempt;
|
||||||
|
import com.yau.digitalrmb.training.attempt.domain.ExperimentAttemptRepository;
|
||||||
|
import com.yau.digitalrmb.training.attempt.domain.ExperimentModule;
|
||||||
|
import com.yau.digitalrmb.training.attempt.domain.ExperimentSubject;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.dao.DuplicateKeyException;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
@Transactional
|
||||||
|
class JdbcExperimentAttemptRepositoryTest {
|
||||||
|
@Autowired
|
||||||
|
private ExperimentAttemptRepository repository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void assignsIncreasingAttemptNumbersWithinTheAuthenticatedScope() {
|
||||||
|
ExperimentSubject subject = new ExperimentSubject("user-1", 10L, 20L, "ORG-1");
|
||||||
|
|
||||||
|
ExperimentAttempt first = repository.create(ExperimentModule.EXCHANGE, subject);
|
||||||
|
first.cancel();
|
||||||
|
repository.save(first);
|
||||||
|
ExperimentAttempt second = repository.create(ExperimentModule.EXCHANGE, subject);
|
||||||
|
|
||||||
|
assertEquals(1, first.getAttemptNo());
|
||||||
|
assertEquals(2, second.getAttemptNo());
|
||||||
|
assertEquals(second.getId(), repository.findCurrent(ExperimentModule.EXCHANGE, subject).get().getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void scopesAttemptReadsToUserSchoolClassAndOrganization() {
|
||||||
|
ExperimentSubject owner = new ExperimentSubject("user-1", 10L, 20L, "ORG-1");
|
||||||
|
ExperimentAttempt attempt = repository.create(ExperimentModule.ISSUANCE, owner);
|
||||||
|
|
||||||
|
assertTrue(repository.findById(attempt.getId(), owner).isPresent());
|
||||||
|
assertFalse(repository.findById(attempt.getId(),
|
||||||
|
new ExperimentSubject("user-2", 10L, 20L, "ORG-1")).isPresent());
|
||||||
|
assertFalse(repository.findById(attempt.getId(),
|
||||||
|
new ExperimentSubject("user-1", 10L, 21L, "ORG-1")).isPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enforcesOneActionRowForAnAttemptStepAndActionCode() {
|
||||||
|
ExperimentSubject subject = new ExperimentSubject("user-1", 10L, 20L, "ORG-1");
|
||||||
|
ExperimentAttempt attempt = repository.create(ExperimentModule.PAYMENT, subject);
|
||||||
|
UUID actionId = UUID.randomUUID();
|
||||||
|
repository.insertAction(ExperimentAction.running(actionId, attempt.getId(), "02",
|
||||||
|
"generate-digest", "fingerprint-1"));
|
||||||
|
|
||||||
|
ExperimentAction locked = repository.findActionForUpdate(attempt.getId(), "02", "generate-digest")
|
||||||
|
.orElseThrow(AssertionError::new);
|
||||||
|
|
||||||
|
assertEquals(ActionStatus.RUNNING, locked.getStatus());
|
||||||
|
assertThrows(DuplicateKeyException.class, () -> repository.insertAction(
|
||||||
|
ExperimentAction.running(UUID.randomUUID(), attempt.getId(), "02",
|
||||||
|
"generate-digest", "fingerprint-2")));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue