GitAuthoritativeRelationMutationService.java
package com.taxonomy.relations.service;
import com.taxonomy.dsl.command.ArchitectureRelationDslTransformer.RelationDefinition;
import com.taxonomy.dsl.command.ArchitectureRelationDslTransformer.RelationIdentity;
import com.taxonomy.relations.command.ArchitectureRelationGitCommandService;
import com.taxonomy.relations.command.ArchitectureRelationGitCommandService.CommandMetadata;
import com.taxonomy.relations.command.ArchitectureRelationGitCommandService.CommandResult;
import com.taxonomy.relations.command.ArchitectureRelationGitCommandService.RelationCommand;
import com.taxonomy.relations.command.ArchitectureRelationGitCommandService.RemoveRelation;
import com.taxonomy.relations.command.ArchitectureRelationGitCommandService.UpsertRelation;
import com.taxonomy.relations.service.RelationBranchProjectionReadinessService.Readiness;
import com.taxonomy.relations.service.RelationBranchProjectionReadinessService.ReadinessState;
import com.taxonomy.relations.service.RelationBranchProjectionRebuildService.RebuildResult;
import com.taxonomy.relations.service.RelationProjectionRecoveryService.RecoveryRecord;
import com.taxonomy.workspace.service.RepositoryContext;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Objects;
/**
* Makes one exact Git commit authoritative before attempting its rebuildable
* relational projection.
*
* <p>The method is deliberately not transactional: a failed projection must
* never roll back or hide the already successful Git command. The exact command
* state is projected first, then the complete branch is rebuilt and proven ready
* before any pending recovery is reconciled. Recovery is persisted in an
* independent transaction and callers always receive the immutable Git authority,
* even when that secondary persistence also fails.</p>
*/
@Service
public class GitAuthoritativeRelationMutationService {
private final ArchitectureRelationGitCommandService commandService;
private final RelationDecisionProjectionService projectionService;
private final RelationBranchProjectionRebuildService rebuildService;
private final RelationBranchProjectionReadinessService readinessService;
private final RelationProjectionRecoveryService recoveryService;
public GitAuthoritativeRelationMutationService(
ArchitectureRelationGitCommandService commandService,
RelationDecisionProjectionService projectionService,
RelationBranchProjectionRebuildService rebuildService,
RelationBranchProjectionReadinessService readinessService,
RelationProjectionRecoveryService recoveryService) {
this.commandService = Objects.requireNonNull(
commandService, "commandService");
this.projectionService = Objects.requireNonNull(
projectionService, "projectionService");
this.rebuildService = Objects.requireNonNull(
rebuildService, "rebuildService");
this.readinessService = Objects.requireNonNull(
readinessService, "readinessService");
this.recoveryService = Objects.requireNonNull(
recoveryService, "recoveryService");
}
public MutationResult upsert(
RepositoryContext context,
String expectedHeadCommit,
RelationDefinition definition,
CommandMetadata metadata) throws IOException {
return execute(
context,
expectedHeadCommit,
new UpsertRelation(definition, metadata));
}
public MutationResult remove(
RepositoryContext context,
String expectedHeadCommit,
RelationIdentity identity,
CommandMetadata metadata) throws IOException {
return execute(
context,
expectedHeadCommit,
new RemoveRelation(identity, metadata));
}
private MutationResult execute(
RepositoryContext context,
String expectedHeadCommit,
RelationCommand command) throws IOException {
CommandResult authority = commandService.execute(
context, expectedHeadCommit, command);
try {
RelationDecisionProjectionService.ProjectionResult projection =
projectionService.project(context, authority, command);
RebuildResult rebuild = rebuildService.rebuild(context);
verifyCompleteProjection(context, authority, rebuild);
recoveryService.reconcileAfterRebuild(
context, authority.authoritativeCommitId());
return new MutationResult(authority, projection);
} catch (RuntimeException projectionFailure) {
RecoveryRecord recovery = null;
try {
recovery = recoveryService.recordPending(
authority, projectionFailure);
} catch (RuntimeException recoveryFailure) {
projectionFailure.addSuppressed(recoveryFailure);
}
throw new ProjectionPendingException(
authority, recovery, projectionFailure);
}
}
private void verifyCompleteProjection(
RepositoryContext context,
CommandResult authority,
RebuildResult rebuild) {
String authoritativeCommit = authority.authoritativeCommitId();
if (!authoritativeCommit.equals(rebuild.authoritativeCommitId())) {
throw new ProjectionCompletionException(
"Complete relation projection rebuilt commit "
+ rebuild.authoritativeCommitId()
+ " instead of command authority "
+ authoritativeCommit);
}
Readiness readiness = readinessService.inspect(context);
boolean complete = readiness.state() == ReadinessState.READY
&& authoritativeCommit.equals(readiness.currentHeadCommit())
&& authoritativeCommit.equals(readiness.projectedCommit())
&& rebuild.relationCount() == readiness.rows().size();
if (!complete) {
throw new ProjectionCompletionException(
"Relation command at " + authoritativeCommit
+ " did not produce a complete readable branch projection: "
+ readiness.state());
}
}
public record MutationResult(
CommandResult authority,
RelationDecisionProjectionService.ProjectionResult projection) {
public MutationResult {
authority = Objects.requireNonNull(authority, "authority");
projection = Objects.requireNonNull(projection, "projection");
}
}
public static final class ProjectionCompletionException
extends IllegalStateException {
public ProjectionCompletionException(String message) {
super(message);
}
}
public static final class ProjectionPendingException
extends IllegalStateException {
private final CommandResult authority;
private final RecoveryRecord recovery;
/**
* Compatibility form for callers that know the Git authority but do
* not have a durable recovery record, for example isolated tests or a
* secondary recovery-store failure.
*/
public ProjectionPendingException(
CommandResult authority,
Throwable cause) {
this(authority, null, cause);
}
public ProjectionPendingException(
CommandResult authority,
RecoveryRecord recovery,
Throwable cause) {
super("Git relation command succeeded at "
+ authority.authoritativeCommitId()
+ ", but its complete projection requires recovery", cause);
this.authority = Objects.requireNonNull(authority, "authority");
this.recovery = recovery;
}
public CommandResult getAuthority() {
return authority;
}
public RecoveryRecord getRecovery() {
return recovery;
}
public boolean isRecoveryPersisted() {
return recovery != null;
}
}
}