Version Patch System
Chronicle includes a version-based patch system to handle breaking changes during upgrades. Patches are automatically discovered and applied during server startup, ensuring the system state is migrated correctly between versions.
Overview
Section titled “Overview”The patch system:
- Automatically discovers patches using reflection (
IInstancesOf<ICanApplyPatch>) - Applies patches in order based on semantic version
- Tracks applied patches to prevent re-execution
- Supports rollback through
Down()methods for safe downgrades - Runs at startup before other Chronicle services initialize
Key Concepts
Section titled “Key Concepts”SemanticVersion
Section titled “SemanticVersion”Patches are tied to semantic versions (e.g., 1.5.0, 2.0.0-beta.1). The system compares the current system version with patch versions to determine which patches to apply.
Patch Lifecycle
Section titled “Patch Lifecycle”- Discovery: On startup, all
ICanApplyPatchimplementations are discovered - Filtering: Only patches with versions newer than the current system version are selected
- Ordering: Selected patches are sorted in ascending version order
- Application: Each patch’s
Up()method is called sequentially - Tracking: Successfully applied patches are recorded in storage
- Version Update: System version is updated to the latest applied patch version
Storage
Section titled “Storage”Patches are tracked in MongoDB:
- Collection:
patches(in system database) - Version: Stored separately as the current system version
- State: PatchManager grain maintains state of all applied patches
Writing a Patch
Section titled “Writing a Patch”Basic Structure
Section titled “Basic Structure”Create a patch by implementing ICanApplyPatch:
using Cratis.Chronicle.Concepts.System;using Cratis.Chronicle.Patching;using Cratis.Chronicle.Storage;using Microsoft.Extensions.Logging;
namespace Cratis.Chronicle.Patches;
public class MyPatch(IStorage storage, ILogger<MyPatch> logger) : ICanApplyPatch{ public SemanticVersion Version => new(1, 5, 0);
public async Task Up() { // Migration logic here logger.LogInformation("Applying MyPatch");
// Access storage as needed var eventStore = storage.GetEventStore(EventStoreName.System); // ... perform migration }
public async Task Down() { // Rollback logic here (optional but recommended) logger.LogInformation("Rolling back MyPatch");
// Reverse the changes made in Up() }}Patch Naming Conventions
Section titled “Patch Naming Conventions”- Folder structure: Organize patches by version in
Source/Kernel/Core/Patches/{version}/ - File naming: Use descriptive names (e.g.,
RenameReactors.cs,MigrateEventSchema.cs) - Class naming: Match the file name (the
Nameproperty is auto-derived from the type name)
Dependencies
Section titled “Dependencies”Patches can inject any services registered in the DI container:
public class ComplexPatch( IStorage storage, IEventTypes eventTypes, ILogger<ComplexPatch> logger) : ICanApplyPatch{ public SemanticVersion Version => new(2, 0, 0);
public async Task Up() { // Use injected services await eventTypes.DiscoverAndRegister(EventStoreName.System); }
public async Task Down() { // Rollback }}Best Practices
Section titled “Best Practices”1. Semantic Logging
Section titled “1. Semantic Logging”Use proper semantic logging with LoggerMessage attributes:
internal static partial class MyPatchLogMessages{ [LoggerMessage(LogLevel.Information, "Starting MyPatch migration")] internal static partial void StartingMigration(this ILogger<MyPatch> logger);
[LoggerMessage(LogLevel.Information, "Migrated {Count} items")] internal static partial void MigratedItems(this ILogger<MyPatch> logger, int count);}
public class MyPatch(IStorage storage, ILogger<MyPatch> logger) : ICanApplyPatch{ public async Task Up() { logger.StartingMigration(); // ... migration logic logger.MigratedItems(count); }}2. Implement Down() for Safety
Section titled “2. Implement Down() for Safety”Always implement Down() to support rollback scenarios:
public async Task Down(){ // Reverse the changes made in Up() // This allows safe rollback if needed}3. Handle Idempotency
Section titled “3. Handle Idempotency”Patches should be idempotent where possible. The system prevents re-execution, but defensive coding helps:
public async Task Up(){ var eventStore = storage.GetEventStore(EventStoreName.System); var reactors = await eventStore.Reactors.GetAll();
// Filter to only process items that need migration var reactorsToMigrate = reactors .Where(r => r.Identifier.Value.Contains("OldPattern")) .ToList();
if (reactorsToMigrate.Count == 0) { logger.NothingToMigrate(); return; }
// Proceed with migration}4. Test Thoroughly
Section titled “4. Test Thoroughly”Write comprehensive specs for your patches:
public class when_applying_my_patch : given.a_my_patch_context{ async Task Because() => await _patch.Up();
[Fact] void should_migrate_data() => /* assertion */; [Fact] void should_preserve_existing_data() => /* assertion */;}Runtime Behavior
Section titled “Runtime Behavior”Startup Sequence
Section titled “Startup Sequence”- Server Initialization: Chronicle server starts
- Patch Discovery:
PatchManagergrain discovers allICanApplyPatchimplementations - Version Check: Current system version is retrieved from storage
- Patch Selection: Patches newer than current version are selected
- Sequential Application: Patches are applied in ascending version order
- Version Update: System version is updated to latest applied patch
- Normal Startup: Chronicle continues with normal initialization
Error Handling
Section titled “Error Handling”If a patch fails:
- The exception is logged and re-thrown
- Startup is halted to prevent running with inconsistent state
- Manual intervention is required to fix the issue
- The patch can be fixed and server restarted
Version Comparison
Section titled “Version Comparison”Patches are applied based on version comparison:
- Current: 1.0.0, Patch: 1.5.0 → Patch IS applied
- Current: 2.0.0, Patch: 1.5.0 → Patch is NOT applied
- Current: 1.5.0, Patch: 1.5.0 → Patch is NOT applied (equal versions)
- Current: null, Patch: 1.0.0 → Patch IS applied (treats null as 0.0.0)
Example: RenameReactors Patch
Section titled “Example: RenameReactors Patch”The RenameReactors patch (version 15.3.0) demonstrates a real-world migration:
public class RenameReactors(IStorage storage, ILogger<RenameReactors> logger) : ICanApplyPatch{ public SemanticVersion Version => new(15, 3, 0);
public async Task Up() { logger.StartingPatch();
var systemEventStore = storage.GetEventStore(EventStoreName.System); var reactors = await systemEventStore.Reactors.GetAll();
var reactorsToRename = reactors .Where(r => r.Identifier.Value.Contains("Grains", StringComparison.OrdinalIgnoreCase)) .ToList();
logger.FoundReactorsToRename(reactorsToRename.Count);
foreach (var reactor in reactorsToRename) { var currentId = reactor.Identifier; var newIdValue = currentId.Value.Replace("Grains", string.Empty, StringComparison.OrdinalIgnoreCase); var newId = new ReactorId(newIdValue);
logger.RenamingReactor(currentId, newId); await systemEventStore.Reactors.Rename(currentId, newId); }
logger.PatchCompleted(); }
public async Task Down() { // Restore "Grains" in reactor names for rollback logger.StartingRollback(); // ... rollback logic logger.RollbackCompleted(); }}Troubleshooting
Section titled “Troubleshooting”Patch Not Running
Section titled “Patch Not Running”- Check version: Ensure patch version is greater than current system version
- Check registration: Verify patch implements
ICanApplyPatchand is in correct namespace - Check logs: Look for patch discovery and application logs at startup
Patch Failing
Section titled “Patch Failing”- Review exception: Check startup logs for detailed error information
- Test locally: Run patch specs to verify logic
- Check storage: Ensure storage connections are working
- Verify state: Check if system is in expected state before migration
Need to Re-run Patch
Section titled “Need to Re-run Patch”If you need to re-run a patch (e.g., during development):
- Remove patch record from
patchescollection in MongoDB - Optionally reset system version if needed
- Restart server
Warning: Only do this in development environments. Production systems should use new patch versions.
Migration from Older Versions
Section titled “Migration from Older Versions”When upgrading Chronicle from versions without the patch system:
- System version defaults to
0.0.0(orSemanticVersion.NotSet) - All patches will be discovered and applied in order
- After successful application, system version is set to latest patch version
- Future upgrades will only apply newer patches
Summary
Section titled “Summary”The patch system provides a robust, automated way to handle breaking changes during Chronicle upgrades. By following the conventions and best practices outlined here, you can write safe, testable patches that keep Chronicle systems properly migrated across versions.