Migration from 3.x
4.x introduces a number of breaking changes in the following libraries: DotNext, DotNext.Threading, DotNext.Net.Cluster, DotNext.AspNetCore.Cluster.
Core Library
WaitAsync and ContinueWithTimeout methods
Both methods have been removed from the library because they can be replaced with WaitAsync method from .NET 6 standard library.
Box<T>
DotNext.Runtime.Box<T>
value type is replaced by DotNext.Runtime.Reference<T>
value type. A new type allows to hold the reference to the various memory locations: array element, static of instance field, boxed value type. As a result, you need to use the factory method to make a reference to the boxed value:
using DotNext.Runtime;
object boxed = 25;
Reference<int> referenceToBoxedValue = Reference.Unbox<int>(boxed);
Console.WriteLine(referenceToBoxedValue.Target);
IO
SequenceBinaryReader
has been renamed to SequenceReader
.
IAsyncBinaryReader and IAsyncBinaryWriter
IAsyncBinaryWriter
prior to 3.x had a set of methods (WriteGuidAsync
, WriteDateTimeAsync
) for encoding various value types as a string using the specified encoding. Starting from .NET 6, there is a special public interface ISpanFormattable that is implemented by all formattable data types in .NET Base Class Library. It's reasonable to offer a single method that can work with any type implementing the necessary interface instead of multiple methods for each formattable data type.
using System.Text;
using DotNext.IO;
IAsyncBinaryWriter writer;
await writer.WriteFormattableAsync<int>(42, LengthFormat.Plain, Encoding.UTF8, "X"); // encode int as as set of bytes using UTF-8 encoding
The same change was applied to IAsyncBinaryReader
interface. All methods for parsing various value types have been replaced with ParseAsync
method. It accepts the delegate responsible for parsing of the value from the sequence of characters represented by ReadOnlySpan<char>
data type:
using System.Text;
using DotNext.IO;
IAsyncBinaryReader reader;
var i = await reader.ParseAsync<int>(static (chars, provider) => int.Parse(chars, provider: provider), Encoding.UTF8);
Threading
QueuedSynchronizer
and Synchronizer
classes as well as ISynchronizer
interface are merged into single QueuedSynchronizer
class. Lock acquisition methods now return ValueTask or ValueTask<T> value type instead of Task class. If you still need a Task
then use AsTask() instance method.
AsyncReaderWriterLock
Presence of upgradeable read lock was a huge mistake in architecture. It allows to upgrade the read lock to the write lock by concurrent flow, e.g.:
using DotNext.Threading;
using var rwLock = new AsyncReaderWriterLock();
// async flow #1
await rwLock.EnterUpgradeableReadLockAsync();
// async flow #2
await rwLock.EnterWriteLockAsync(); // this flow can upgrade the existing read lock
To avoid that, starting from 4.0 version of the library you need to upgrade the lock manually:
using DotNext.Threading;
using var rwLock = new AsyncReaderWriterLock();
await rwLock.EnterReadLockAsync();
await rwLock.UpgradeToWriteLockAsync();
rwLock.DowngradeFromWriteLock();
rwLock.Release();
UpgradeToWriteLockAsync
should be called by the same flow after the invocation of EnterReadLockAsync
method.
ExitReadLock
and ExitWriteLock
methods replaced with a single Release
method.
AsyncTrigger
AsyncTrigger
divided to two classes: generic AsyncTrigger<T>
class and non-generic AsyncTrigger
class. For producer-consumer scenario without coordinated state you need to use non-generic version. If you have coordinated state then use generic version. Read more here for detailed information about the differences.
Cluster Programming Suite
DotNext.Net.Cluster
and DotNext.AspNetCore.Cluster
have breaking changes in Raft implementation.
ICluster interface
All custom delegate types for Cluster events have been replaced with delegate types from .NET Base Class Library. Members
property is replaced by IPeerMesh.Peers
property.
IExpandableCluster interface
The interface is completely replaced with IPeerMesh
interface that has more reusable design.
IP safelist
allowedNetworks
configuration parameter is no longer available. It's easily to implement it by the user through ASP.NET Core Middleware and amazing IPNetwork library. Also, there is perfect article about how to organize IP safelist.
Interpreter Framework
DotNext.Runtime.Serialization.IFormatter<T>
interface has been replaced with DotNext.Runtime.Serialization.ISerializable<TSelf>
interface so now the type itself must control its serialization/deserialization logic. ISerializable<TSelf>
interface is more reusable across various scenarios and not tightly coupled with Interpreter Framework anymore.
The next thing is CommandAttribute<T>
. Thanks to C# 10, we can use generic attributes. Therefore, you don't need to annotate command DTO with this attribute. Instead, you need to apply the attribute for each command type on the class that derives from CommandInterpreter
class:
using DotNext.Net.Cluster.Consensus.Raft.Commands;
[Command<BinaryOperationCommand>(BinaryOperationCommand.Id)]
[Command<UnaryOperationCommand>(UnaryOperationCommand.Id)]
[Command<SnapshotCommand>(SnapshotCommand.Id)]
sealed class CustomInterpreter : CommandInterpreter
{
}
Typed Messages
MessageHandler
uses the same concept of generic attributes as Interpreter Framework. All typed messages must be registered with generic MessageAttribute<T>
attribute instead of annotating individual types:
using DotNext.Net.Cluster.Messaging;
[Message<AddMessage>(AddMessage.Name)]
[Message<SubtractMessage>(SubtractMessage.Name)]
[Message<ResultMessage>(ResultMessage.Name)]
public class TestMessageHandler : MessageHandler
{
}
Each typed message must implement DotNext.Runtime.Serialization.ISerializable<TSelf>
interface to provide serialization/deserialization logic.
MessageClient
is no longer use MessageAttribute<T>
for registering message types. Instead, message types can be registered using RegisterMessage
method as follows:
using DotNext.Net.Cluster.Messaging;
var typedClient = new MessagingClient(client)
.RegisterMessage<AddMessage>(AddMessage.Name)
.RegisterMessage<SubtractMessage>(SubtractMessage.Name)
.RegisterMessage<ResultMessage>(ResultMessage.Name);
Configuration of cluster members
members
configuration property is no longer available. The way of configuring, adding and removing of cluster members is changed completely. It happened because Raft provides a mechanism of cluster configuration management on top of itself without involving external discovery. Raft-native cluster membership protocol allows to avoid some critical issues associated with the external discovery mechanism. One of them - a probability of having two leaders in the same cluster during modification of the cluster configuration.
A new cluster configuration management model introduces DotNext.Net.Cluster.Consensus.Raft.Membership.IClusterConfigurationStorage
interface. Currently, there are two storages supported:
- In-memory configuration suitable for testing purposes
- Persistent configuration suitable for production use
Raft-native membership protocol offers the easy and safe way of adding and removing new cluster members. All we need is to provide a guarantee that only one member can be removed or added at a time in the same cluster. When the modified configuration will be committed by the majority of nodes, you can perform the next change.
Therefore, IClusterConfigurationStorage
actually holds the two configurations:
- Active configuration which is applied and commited by the majority of nodes
- Proposed configuration which reflects the requested change (added or removed member) and must be replicated and committed by the majority of nodes.
IClusterConfigurationStorage<TAddress>
generic interface allows to add or remove members dynamically and wait until the proposed configuration turned into active configuration.
This mechanism for membership management dictates how the node must be bootstrapped:
coldStart=true
(Cold Start mode) configuration property means that you want to start a first node in the cluster. It means that the node adds itself to the cluster configuration as proposed change and wait for other nodescoldStart=false
(Announcement mode) configuration property means that the node must be added to existing cluster.
Any change in configuration must be processed by the leader node as well as any other write operation.
IRaftHttpCluster
offers high-level API for managing cluster members:
AddMemberAsync
that allows to announce and warmup the nodeRemoveMemberAsync
that allows to remove the node from the cluster
These methods are callable on leader node only. It's possible to automate announcement of a new node. ClusterMemberAnnouncer<TAddress>
delegate instance can be registered in DI container and then called automatically when the node started in Announcement mode. In case of HTTP, the announcer can utilize redirection to leader capability and send the announcement to the leader node. Then, the announcement endpoint on the leader node can call AddMemberAsync
method to propose and commit a new cluster configuration. Read this for more information.
Persistent WAL
PersistentState class exists but cannot be inherited directly anymore. Now this is a base type for disk-based and memory-based state machine. Derive from MemoryBasedStateMachine class to migrate existing code.