Utility class that coordinates a safe replacement of an authoritative body. It runs a sequence of callbacks to activate a candidate, quiesce the previous body, publish the candidate, and then retire the previous, rolling back and cleaning up on any failure or exception.
#nullable enable
using System;
using System.Collections.Generic;
using Hexagon.V2.Kernel;
namespace Hexagon.V2.Runtime;
/// <summary>
/// Engine-neutral transaction coordinator for replacing a live authoritative body.
/// The previous body remains recoverable until the candidate is active and its
/// canonical pointer has been published. Every failed stage rolls back and discards
/// the partial candidate before failure is reported.
/// </summary>
internal static class AuthoritativeBodyReplacement
{
public static OperationResult<T> RequireActivated<T>(
T candidate,
Func<bool> activateCandidate,
Func<bool> quiescePrevious,
Func<bool> publishCandidate,
Action rollbackPublication,
Action restorePrevious,
Action discardCandidate,
Action retirePrevious )
{
ArgumentNullException.ThrowIfNull( candidate );
ArgumentNullException.ThrowIfNull( activateCandidate );
ArgumentNullException.ThrowIfNull( quiescePrevious );
ArgumentNullException.ThrowIfNull( publishCandidate );
ArgumentNullException.ThrowIfNull( rollbackPublication );
ArgumentNullException.ThrowIfNull( restorePrevious );
ArgumentNullException.ThrowIfNull( discardCandidate );
ArgumentNullException.ThrowIfNull( retirePrevious );
var previousQuiesceStarted = false;
var publicationStarted = false;
try
{
if ( !activateCandidate() )
return Fail<T>( "Candidate activation was rejected.", false, false,
rollbackPublication, restorePrevious, discardCandidate );
previousQuiesceStarted = true;
if ( !quiescePrevious() )
return Fail<T>( "Previous-body quiescence was rejected.", false, true,
rollbackPublication, restorePrevious, discardCandidate );
publicationStarted = true;
if ( !publishCandidate() )
return Fail<T>( "Canonical body publication was rejected.", true, true,
rollbackPublication, restorePrevious, discardCandidate );
// Retirement must be implemented as a no-throw destroy-or-retain operation:
// publication is already complete and cannot truthfully be reported as failed.
retirePrevious();
return OperationResult<T>.Success( candidate );
}
catch ( Exception exception )
{
return Fail<T>(
$"Authoritative body replacement threw: {exception.Message}",
publicationStarted,
previousQuiesceStarted,
rollbackPublication,
restorePrevious,
discardCandidate );
}
}
private static OperationResult<T> Fail<T>(
string message,
bool publicationStarted,
bool previousQuiesceStarted,
Action rollbackPublication,
Action restorePrevious,
Action discardCandidate )
{
var cleanupFailures = new List<string>();
if ( publicationStarted ) TryCleanup( rollbackPublication, "publication rollback", cleanupFailures );
if ( previousQuiesceStarted ) TryCleanup( restorePrevious, "previous-body restoration", cleanupFailures );
TryCleanup( discardCandidate, "candidate cleanup", cleanupFailures );
if ( cleanupFailures.Count > 0 )
message += $" Recovery degraded: {string.Join( "; ", cleanupFailures )}";
return OperationResult<T>.Failure( ErrorCode.InternalError, message );
}
private static void TryCleanup( Action action, string stage, ICollection<string> failures )
{
try { action(); }
catch ( Exception exception ) { failures.Add( $"{stage}: {exception.Message}" ); }
}
}