RelationBranchProjectionReadinessService.java
package com.taxonomy.relations.service;
import com.taxonomy.dsl.storage.DslGitRepository;
import com.taxonomy.dsl.storage.DslGitRepositoryFactory;
import com.taxonomy.relations.model.RelationDecisionProjection;
import com.taxonomy.relations.model.RelationDecisionProjectionCheckpoint;
import com.taxonomy.relations.repository.RelationDecisionProjectionCheckpointRepository;
import com.taxonomy.relations.repository.RelationDecisionProjectionRepository;
import com.taxonomy.workspace.service.RepositoryContext;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
/**
* Proves whether one branch projection is complete for the current Git head.
*
* <p>Missing, stale, partial, tombstone-contaminated or concurrently advanced
* projections are never returned as ready.</p>
*/
@Service
public class RelationBranchProjectionReadinessService {
private final DslGitRepositoryFactory gitRepositoryFactory;
private final RelationDecisionProjectionRepository projectionRepository;
private final RelationDecisionProjectionCheckpointRepository checkpointRepository;
public RelationBranchProjectionReadinessService(
DslGitRepositoryFactory gitRepositoryFactory,
RelationDecisionProjectionRepository projectionRepository,
RelationDecisionProjectionCheckpointRepository checkpointRepository) {
this.gitRepositoryFactory = Objects.requireNonNull(
gitRepositoryFactory, "gitRepositoryFactory");
this.projectionRepository = Objects.requireNonNull(
projectionRepository, "projectionRepository");
this.checkpointRepository = Objects.requireNonNull(
checkpointRepository, "checkpointRepository");
}
/**
* Returns the current Git head commit for the given branch without loading
* or validating projection rows. Use this when only the head SHA is needed.
*/
public String readCurrentHead(RepositoryContext context) {
Objects.requireNonNull(context, "context");
DslGitRepository repository = gitRepositoryFactory.resolveRepository(context);
return readHead(repository, context.branch());
}
@Transactional(readOnly = true)
public Readiness inspect(RepositoryContext context) {
Objects.requireNonNull(context, "context");
DslGitRepository repository = gitRepositoryFactory.resolveRepository(context);
String headBefore = readHead(repository, context.branch());
if (headBefore == null) {
return Readiness.notReady(
ReadinessState.BRANCH_MISSING,
null,
null,
List.of());
}
String workspaceScopeKey = RelationDecisionProjection.scopeKeyFor(
context.workspaceId());
RelationDecisionProjectionCheckpoint checkpoint = checkpointRepository
.findByRepositoryIdAndWorkspaceScopeKeyAndBranch(
context.repositoryId(),
workspaceScopeKey,
context.branch())
.orElse(null);
if (checkpoint == null) {
return Readiness.notReady(
ReadinessState.NOT_BUILT,
headBefore,
null,
List.of());
}
if (!headBefore.equals(checkpoint.getAuthoritativeCommitId())) {
return Readiness.notReady(
ReadinessState.STALE,
headBefore,
checkpoint.getAuthoritativeCommitId(),
List.of());
}
List<RelationDecisionProjection> rows = projectionRepository
.findByRepositoryIdAndWorkspaceScopeKeyAndBranchOrderBySourceCodeAscRelationTypeAscTargetCodeAsc(
context.repositoryId(),
workspaceScopeKey,
context.branch());
boolean inconsistent = rows.size() != checkpoint.getRelationCount()
|| rows.stream().anyMatch(row -> !row.isRelationPresent()
|| !headBefore.equals(row.getAuthoritativeCommitId()));
if (inconsistent) {
return Readiness.notReady(
ReadinessState.CORRUPT,
headBefore,
checkpoint.getAuthoritativeCommitId(),
List.of());
}
String headAfter = readHead(repository, context.branch());
if (!Objects.equals(headBefore, headAfter)) {
return Readiness.notReady(
ReadinessState.STALE,
headAfter,
checkpoint.getAuthoritativeCommitId(),
List.of());
}
return new Readiness(
ReadinessState.READY,
headBefore,
checkpoint.getAuthoritativeCommitId(),
List.copyOf(rows));
}
public List<RelationDecisionProjection> requireReady(RepositoryContext context) {
Readiness readiness = inspect(context);
if (readiness.state() != ReadinessState.READY) {
String workspaceScopeKey = RelationDecisionProjection.scopeKeyFor(
context.workspaceId());
throw new RelationProjectionNotReadyException(
"Relation projection is not ready for repository "
+ context.repositoryId()
+ ", workspace scope " + workspaceScopeKey
+ ", branch " + context.branch()
+ ": " + readiness.state());
}
return readiness.rows();
}
private static String readHead(
DslGitRepository repository,
String branch) {
try {
Ref ref = repository.getGitRepository().getRefDatabase()
.exactRef(Constants.R_HEADS + branch);
return ref == null || ref.getObjectId() == null
? null
: ref.getObjectId().name();
} catch (IOException error) {
throw new RelationProjectionNotReadyException(
"Unable to read selected relation projection branch head",
error);
}
}
public enum ReadinessState {
READY,
NOT_BUILT,
STALE,
CORRUPT,
BRANCH_MISSING
}
public record Readiness(
ReadinessState state,
String currentHeadCommit,
String projectedCommit,
List<RelationDecisionProjection> rows) {
public Readiness {
state = Objects.requireNonNull(state, "state");
rows = List.copyOf(Objects.requireNonNull(rows, "rows"));
if (state != ReadinessState.READY && !rows.isEmpty()) {
throw new IllegalArgumentException(
"Non-ready projection must not expose relation rows");
}
}
private static Readiness notReady(
ReadinessState state,
String currentHeadCommit,
String projectedCommit,
List<RelationDecisionProjection> rows) {
return new Readiness(
state, currentHeadCommit, projectedCommit, rows);
}
}
public static final class RelationProjectionNotReadyException
extends IllegalStateException {
public RelationProjectionNotReadyException(String message) {
super(message);
}
public RelationProjectionNotReadyException(
String message,
Throwable cause) {
super(message, cause);
}
}
}