diff --git a/src/Adaptive.Archiver.IntegrationTests/PersistentSubscriptionTest.cs b/src/Adaptive.Archiver.IntegrationTests/PersistentSubscriptionTest.cs index b0dd5eb6..319a9cd1 100644 --- a/src/Adaptive.Archiver.IntegrationTests/PersistentSubscriptionTest.cs +++ b/src/Adaptive.Archiver.IntegrationTests/PersistentSubscriptionTest.cs @@ -16,6 +16,7 @@ using System; using System.Collections.Generic; +using Adaptive.Agrona; using Adaptive.Aeron.LogBuffer; using Adaptive.Archiver.IntegrationTests.Helpers; using Adaptive.Archiver.IntegrationTests.Infrastructure; @@ -84,32 +85,13 @@ public virtual void SetUp() [TearDown] public virtual void TearDown() { - foreach (var c in System.Linq.Enumerable.Reverse(Closeables)) - { - DisposeWithTimeout(c, 3_000, "Closeable"); - } + CloseHelper.CloseAll(System.Linq.Enumerable.Reverse(Closeables)); Closeables.Clear(); - DisposeWithTimeout(AeronArchive, 3_000, "AeronArchive"); - DisposeWithTimeout(Aeron, 3_000, "Aeron"); - - try - { - Archive?.Dispose(); - } - catch - { - // Ignored - } - - try - { - Driver?.Dispose(); - } - catch - { - // Ignored - } + CloseHelper.Dispose(AeronArchive); + CloseHelper.Dispose(Aeron); + CloseHelper.Dispose(Archive); + CloseHelper.Dispose(Driver); } private static void DisposeWithTimeout(IDisposable target, int timeoutMs, string name) diff --git a/src/Adaptive.Cluster.Tests/Service/ClusteredServiceContainerContextTest.cs b/src/Adaptive.Cluster.Tests/Service/ClusteredServiceContainerContextTest.cs new file mode 100644 index 00000000..5b16fb06 --- /dev/null +++ b/src/Adaptive.Cluster.Tests/Service/ClusteredServiceContainerContextTest.cs @@ -0,0 +1,91 @@ +/* + * Copyright 2026 Adaptive Financial Consulting Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System; +using System.IO; +using Adaptive.Aeron; +using Adaptive.Aeron.LogBuffer; +using Adaptive.Agrona; +using Adaptive.Agrona.Concurrent; +using Adaptive.Agrona.Concurrent.Status; +using Adaptive.Cluster.Service; +using FakeItEasy; +using NUnit.Framework; +using AeronType = Adaptive.Aeron.Aeron; + +namespace Adaptive.Cluster.Tests.Service +{ + public class ClusteredServiceContainerContextTest + { + private ClusteredServiceContainer.Context _context; + private DirectoryInfo _clusterDir; + + [SetUp] + public void Before() + { + _clusterDir = new DirectoryInfo( + Path.Combine(Path.GetTempPath(), "cluster-ctx-test-" + Guid.NewGuid())); + + AeronType aeron = A.Fake(); + AeronType.Context aeronCtx = A.Fake(); + A.CallTo(() => aeronCtx.AeronDirectoryName()).Returns("test-aeron-dir"); + A.CallTo(() => aeronCtx.SubscriberErrorHandler()).Returns(RethrowingErrorHandler.INSTANCE); + A.CallTo(() => aeronCtx.FilePageSize()).Returns(LogBufferDescriptor.PAGE_MIN_SIZE); + A.CallTo(() => aeron.Ctx).Returns(aeronCtx); + + UnsafeBuffer metaDataBuffer = new UnsafeBuffer(new byte[128 * 1024]); + UnsafeBuffer valuesBuffer = new UnsafeBuffer(new byte[64 * 1024]); + CountersManager countersManager = new CountersManager(metaDataBuffer, valuesBuffer); + + A.CallTo(() => aeron.AddCounter(A._, A._, A._, A._, A._, + A._, A._)) + .ReturnsLazily((int typeId, IDirectBuffer kb, int ko, int kl, IDirectBuffer lb, int lo, int ll) => + new Counter(countersManager, countersManager.Allocate("my-counter", typeId))); + + _context = new ClusteredServiceContainer.Context() + .AeronClient(aeron) + .ClusterDir(_clusterDir) + .ServiceId(0) + .ClusteredService(A.Fake()); + } + + [TearDown] + public void After() + { + _context?.Dispose(); + _context?.DeleteDirectory(); + } + + [Test] + public void ShouldUseDefaultVersionValidatorWhenNoneSuppliedToContext() + { + _context.Conclude(); + + Assert.That(_context.AppVersionValidator(), Is.SameAs(AppVersionValidator.SEMANTIC_VERSIONING_VALIDATOR)); + } + + [Test] + public void ShouldPreserveCustomVersionValidatorSuppliedToContextThroughConclude() + { + var customValidator = A.Fake(); + _context.AppVersionValidator(customValidator); + + _context.Conclude(); + + Assert.That(_context.AppVersionValidator(), Is.SameAs(customValidator)); + } + } +} diff --git a/src/Adaptive.Cluster/AppVersionValidator.cs b/src/Adaptive.Cluster/AppVersionValidator.cs index 45ddd5a7..f79f482c 100644 --- a/src/Adaptive.Cluster/AppVersionValidator.cs +++ b/src/Adaptive.Cluster/AppVersionValidator.cs @@ -19,25 +19,20 @@ namespace Adaptive.Cluster { /// - /// Class to be used for determining AppVersion compatibility. + /// Default used for determining AppVersion compatibility, which uses + /// major version for checking compatibility. /// - /// Default is to use major version for checking compatibility. + /// A custom policy can be supplied by implementing . /// /// - public class AppVersionValidator + public class AppVersionValidator : IVersionValidator { /// /// Singleton instance of version which can be used to avoid allocation. /// public static readonly AppVersionValidator SEMANTIC_VERSIONING_VALIDATOR = new AppVersionValidator(); - /// - /// Check version compatibility between configured context appVersion and appVersion in new leadership term or - /// snapshot. - /// - /// configured appVersion value from context. - /// to check against configured appVersion. - /// true for compatible or false for not compatible. + /// public bool IsVersionCompatible(int contextAppVersion, int appVersionUnderTest) { return SemanticVersion.Major(contextAppVersion) == SemanticVersion.Major(appVersionUnderTest); diff --git a/src/Adaptive.Cluster/IVersionValidator.cs b/src/Adaptive.Cluster/IVersionValidator.cs new file mode 100644 index 00000000..12e3a567 --- /dev/null +++ b/src/Adaptive.Cluster/IVersionValidator.cs @@ -0,0 +1,38 @@ +/* + * Copyright 2026 Adaptive Financial Consulting Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using Adaptive.Agrona; + +namespace Adaptive.Cluster +{ + /// + /// Interface for determining AppVersion compatibility, so that a custom policy can be supplied via + /// . + /// The default implementation is which uses + /// major version for checking compatibility. + /// + public interface IVersionValidator + { + /// + /// Check version compatibility between configured context appVersion and appVersion in new leadership term or + /// snapshot. + /// + /// configured appVersion value from context. + /// to check against configured appVersion. + /// true for compatible or false for not compatible. + bool IsVersionCompatible(int contextAppVersion, int appVersionUnderTest); + } +} diff --git a/src/Adaptive.Cluster/PublicAPI.Unshipped.txt b/src/Adaptive.Cluster/PublicAPI.Unshipped.txt index f7e1757b..31c83808 100644 --- a/src/Adaptive.Cluster/PublicAPI.Unshipped.txt +++ b/src/Adaptive.Cluster/PublicAPI.Unshipped.txt @@ -4,6 +4,10 @@ Adaptive.Cluster.Codecs.JoinLogDecoder.IsStandby() -> Adaptive.Cluster.Codecs.Bo Adaptive.Cluster.Codecs.JoinLogEncoder.IsStandby(Adaptive.Cluster.Codecs.BooleanType value) -> Adaptive.Cluster.Codecs.JoinLogEncoder Adaptive.Cluster.Codecs.NewLeadershipTermDecoder.CommitPosition() -> long Adaptive.Cluster.Codecs.NewLeadershipTermEncoder.CommitPosition(long value) -> Adaptive.Cluster.Codecs.NewLeadershipTermEncoder +Adaptive.Cluster.IVersionValidator +Adaptive.Cluster.IVersionValidator.IsVersionCompatible(int contextAppVersion, int appVersionUnderTest) -> bool +Adaptive.Cluster.Service.ClusteredServiceContainer.Context.AppVersionValidator() -> Adaptive.Cluster.IVersionValidator +Adaptive.Cluster.Service.ClusteredServiceContainer.Context.AppVersionValidator(Adaptive.Cluster.IVersionValidator versionValidator) -> Adaptive.Cluster.Service.ClusteredServiceContainer.Context const Adaptive.Cluster.Codecs.AddPassiveMemberDecoder.SCHEMA_VERSION = 16 -> ushort const Adaptive.Cluster.Codecs.AddPassiveMemberEncoder.SCHEMA_VERSION = 16 -> ushort const Adaptive.Cluster.Codecs.AdminRequestDecoder.SCHEMA_VERSION = 16 -> ushort @@ -266,3 +270,5 @@ static Adaptive.Cluster.Codecs.NewLeadershipTermEncoder.CommitPositionNullValue( *REMOVED*const Adaptive.Cluster.Codecs.TimerEventEncoder.SCHEMA_VERSION = 14 -> ushort *REMOVED*const Adaptive.Cluster.Codecs.VoteDecoder.SCHEMA_VERSION = 14 -> ushort *REMOVED*const Adaptive.Cluster.Codecs.VoteEncoder.SCHEMA_VERSION = 14 -> ushort +*REMOVED*Adaptive.Cluster.Service.ClusteredServiceContainer.Context.AppVersionValidator() -> Adaptive.Cluster.AppVersionValidator +*REMOVED*Adaptive.Cluster.Service.ClusteredServiceContainer.Context.AppVersionValidator(Adaptive.Cluster.AppVersionValidator appVersionValidator) -> Adaptive.Cluster.Service.ClusteredServiceContainer.Context diff --git a/src/Adaptive.Cluster/Service/ClusteredServiceContainer.cs b/src/Adaptive.Cluster/Service/ClusteredServiceContainer.cs index 6060e126..8dedf46b 100644 --- a/src/Adaptive.Cluster/Service/ClusteredServiceContainer.cs +++ b/src/Adaptive.Cluster/Service/ClusteredServiceContainer.cs @@ -690,7 +690,7 @@ public class Context private Aeron.Aeron _aeron; private DutyCycleTracker _dutyCycleTracker; private SnapshotDurationTracker _snapshotDurationTracker; - private AppVersionValidator _appVersionValidator; + private IVersionValidator _versionValidator; private bool _ownsAeronClient; private IClusteredService _clusteredService; @@ -748,9 +748,9 @@ public void Conclude() _idleStrategySupplier = Configuration.IdleStrategySupplier(null); } - if (null == _appVersionValidator) + if (null == _versionValidator) { - _appVersionValidator = Cluster.AppVersionValidator.SEMANTIC_VERSIONING_VALIDATOR; + _versionValidator = Cluster.AppVersionValidator.SEMANTIC_VERSIONING_VALIDATOR; } if (null == _epochClock) @@ -1026,11 +1026,11 @@ public int AppVersion() /// /// /// - /// for user application. + /// for user application. /// this for fluent API. - public Context AppVersionValidator(AppVersionValidator appVersionValidator) + public Context AppVersionValidator(IVersionValidator versionValidator) { - this._appVersionValidator = appVersionValidator; + this._versionValidator = versionValidator; return this; } @@ -1041,10 +1041,10 @@ public Context AppVersionValidator(AppVersionValidator appVersionValidator) /// /// /// - /// AppVersionValidator in use. - public AppVersionValidator AppVersionValidator() + /// IVersionValidator in use. + public IVersionValidator AppVersionValidator() { - return _appVersionValidator; + return _versionValidator; } ///