Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions BREAKING_CHANGES_V1.6.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ of `Behavior`, `Wire`, `API` (combine with `+`).

| Status | PR / Branch | Component | Type | Change | Migration |
|--------|-------------|-----------|------|--------|-----------|
| Planned | [#8317](https://github.com/akkadotnet/akka.net/pull/8317) | `Akka.Remote` / `Akka.Cluster` | API | System/address UID widened `int` → `long` across `AddressUid.Uid`, `AddressUidExtension.Uid()`, `UniqueAddress(Address, int)`/`.Uid`, `QuarantinedEvent(Address, int)`/`.Uid`, `RemoteWatcher.HeartbeatRsp(int)`/`.AddressUid`, and all `Quarantine(Address, int? uid)` members (`IRemoteActorRefProvider` / `RemoteTransport` / `RemoteWatcher`). Hard re-type (v1.6). Prerequisite for Artery. | Recompile against the `long` members; replace `int` uid locals with `long`. Default UID generation stays in 32-bit range; full 64-bit generation is opt-in via `akka.remote.use-64bit-system-uids = on`. |
| Planned | [#8317](https://github.com/akkadotnet/akka.net/pull/8317) | `Akka.Cluster` | Wire | Cluster gossip `UniqueAddress.uid` widened `uint32` → `uint64` (`ClusterMessages.proto`). Same varint wire type → binary-compatible for uids ≤ `uint32.MaxValue`. | Rolling upgrade safe provided uid generation stays in 32-bit range (the default); enable `akka.remote.use-64bit-system-uids` only after the whole cluster + all remote peers are on v1.6. |
| Planned | `feature/deprecate-actorpublisher` | `Akka.Streams` | Behavior | `Source.ActorRef<T>` is re-implemented as a stream-native `GraphStage` (off the legacy `ActorPublisher`). Its public signature is unchanged, but the materialized `IActorRef` no longer treats `PoisonPill` / `Kill` as stream completion — those messages are now ignored. (`Status.Success` draining and `Status.Failure` completion are unchanged.) | Complete the stream explicitly: send `new Status.Success(...)` to drain buffered elements and complete, or `new Status.Failure(ex)` to fail. Do not rely on `PoisonPill` / `Kill`. |
6 changes: 6 additions & 0 deletions openspec/changes/serializer-v2/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ Source-generated MessagePack serializers should be implemented only after this f

Rationale: sourcegen validates the V2 API through real serialization, classic remoting, and persistence paths before Artery envelopes depend on it.

### 11. System UID Is 64-Bit In Any V2 Schema

Any V2 or sourcegen schema that carries the address/system UID (`UniqueAddress`, quarantine, heartbeat, handshake, Artery envelope origin) MUST emit and read it as a 64-bit integer (`long`). No V2 schema may introduce a 32-bit uid field.

Rationale: `widen-system-uid-to-64bit` (Milestone 3.5) re-types the system UID to `long` end-to-end as a prerequisite for Artery, whose frame header carries a 64-bit origin UID. This change does not block on serializer-v2, but constrains its schema (see that change's design.md, Decision 4).

## Risks / Trade-offs

**Compatibility inheritance tension**: `SerializerV2` being usable as `Serializer` keeps this PR compatible, but permits awkward compositions such as wrapping V2 with `SerializerV1Adapter`. Guardrails should be added before native V2 serializers become common.
Expand Down
48 changes: 24 additions & 24 deletions openspec/changes/widen-system-uid-to-64bit/tasks.md
Original file line number Diff line number Diff line change
@@ -1,47 +1,47 @@
## 1. Core type + generation

- [ ] 1.1 Widen `AddressUid.Uid` (field) + `AddressUidExtension.Uid(ActorSystem)` `int → long`
- [ ] 1.2 Add a netstandard2.0-safe 64-bit RNG; default generation stays in `[0, int.MaxValue]`
- [ ] 1.3 Config switch to enable full 64-bit uid generation (default off; documented "all nodes v1.6 first" precondition)
- [x] 1.1 Widen `AddressUid.Uid` (field) + `AddressUidExtension.Uid(ActorSystem)` `int → long`
- [x] 1.2 Add a netstandard2.0-safe 64-bit RNG; default generation stays in `[0, int.MaxValue]`
- [x] 1.3 Config switch to enable full 64-bit uid generation (default off; documented "all nodes v1.6 first" precondition) — `akka.remote.use-64bit-system-uids`

## 2. Cluster

- [ ] 2.1 `UniqueAddress(Address, long uid)` + `long Uid`; fix identity/sort/hash (`Member`)
- [ ] 2.2 Verify gossip vector-clock node name (`VclockName` = address + "-" + uid decimal string) — behavior unchanged
- [ ] 2.3 `ClusterDaemon` `Quarantined(new UniqueAddress(addr, long uid))`
- [x] 2.1 `UniqueAddress(Address, long uid)` + `long Uid`; fix identity/sort/hash (`Member`)
- [x] 2.2 Verify gossip vector-clock node name (`VclockName` = address + "-" + uid decimal string) — behavior unchanged (string-only, `ClusterDaemon.VclockName`; hash also value-identical for int-range uids)
- [x] 2.3 `ClusterDaemon` `Quarantined(new UniqueAddress(addr, long uid))`

## 3. Remoting state machine (internal `int → long`)

- [ ] 3.1 `HandshakeInfo` ctor + `Uid`; `AkkaProtocolTransport` refuseUid path
- [ ] 3.2 `EndpointRegistry` (register/quarantine/refuseUid)
- [ ] 3.3 `Endpoint.cs`: `HopelessAssociation`, `ReliableDeliverySupervisor`, `GotUid`, `EndpointWriter` uid fields/params
- [ ] 3.4 `EndpointManager` messages: `Pass`, `Quarantined`, `Quarantine`, `ResendState`
- [x] 3.1 `HandshakeInfo` ctor + `Uid`; `AkkaProtocolTransport` refuseUid path
- [x] 3.2 `EndpointRegistry` (register/quarantine/refuseUid)
- [x] 3.3 `Endpoint.cs`: `HopelessAssociation`, `ReliableDeliverySupervisor`, `GotUid`, `EndpointWriter` uid fields/params
- [x] 3.4 `EndpointManager` messages: `Pass`, `Quarantined`, `Quarantine`, `ResendState`

## 4. RemoteWatcher

- [ ] 4.1 `HeartbeatRsp(long addressUid)` + `long AddressUid`; `_addressUids: Dictionary<Address,long>`; `ReceiveHeartbeatRsp`
- [ ] 4.2 `Quarantine(Address, long? addressUid)`
- [x] 4.1 `HeartbeatRsp(long addressUid)` + `long AddressUid`; `_addressUids: Dictionary<Address,long>`; `ReceiveHeartbeatRsp`
- [x] 4.2 `Quarantine(Address, long? addressUid)`

## 5. Quarantine API (hard re-type)

- [ ] 5.1 `IRemoteActorRefProvider.Quarantine(Address, long? uid)`
- [ ] 5.2 `RemoteActorRefProvider.Quarantine`, `RemoteTransport.Quarantine`, `Remoting.Quarantine`
- [ ] 5.3 `QuarantinedEvent(Address, long uid)` + `long Uid`
- [x] 5.1 `IRemoteActorRefProvider.Quarantine(Address, long? uid)`
- [x] 5.2 `RemoteActorRefProvider.Quarantine`, `RemoteTransport.Quarantine`, `Remoting.Quarantine`
- [x] 5.3 `QuarantinedEvent(Address, long uid)` + `long Uid`

## 6. Wire + serializers

- [ ] 6.1 `ClusterMessages.proto` `UniqueAddress.uid` `uint32 → uint64`; regenerate
- [ ] 6.2 Remove narrowing casts: `ClusterMessageSerializer`, `MiscMessageSerializer`, `AkkaPduCodec`, DData `SerializationSupport.UniqueAddressFromProto`
- [ ] 6.3 Coordinate `serializer-v2` schema to emit/read 64-bit uid
- [x] 6.1 `ClusterMessages.proto` `UniqueAddress.uid` `uint32 → uint64`; regenerate (Grpc.Tools at build time)
- [x] 6.2 Remove narrowing casts: `ClusterMessageSerializer`, `MiscMessageSerializer`, `AkkaPduCodec`, DData `SerializationSupport.UniqueAddressFromProto`
- [x] 6.3 Coordinate `serializer-v2` schema to emit/read 64-bit uid (Decision 11 in serializer-v2 design.md)

## 7. API approval + build

- [ ] 7.1 Update `Akka.API.Tests` approved files (`ApproveRemote.*`, `ApproveCluster.*`) for the hard break
- [ ] 7.2 `dotnet build -warnaserror` clean
- [ ] 7.3 Akka.Remote + Akka.Cluster + DistributedData tests green
- [x] 7.1 Update `Akka.API.Tests` approved files (`ApproveRemote.*`, `ApproveCluster.*`) for the hard break
- [x] 7.2 `dotnet build -warnaserror` clean
- [x] 7.3 Akka.Remote + Akka.Cluster + DistributedData tests green (391 / 376 / 190 passed, 0 failed, net10.0 Release)

## 8. Compatibility + docs

- [ ] 8.1 Rolling-upgrade test: v1.5 ↔ v1.6 gossip with int-range uids — no truncation, cluster forms
- [ ] 8.2 Full-range generation test (all-v1.6) — >32-bit uids round-trip
- [ ] 8.3 Add `BREAKING_CHANGES_V1.6.md` entry (API + Wire rows)
- [x] 8.1 Rolling-upgrade test: v1.5 ↔ v1.6 gossip with int-range uids — no truncation, cluster forms (wire-level simulation via uint32/uint64 varint cross-parse in `UniqueAddressWireCompatSpec`; a live mixed-version cluster cannot be exercised in-repo)
- [x] 8.2 Full-range generation test (all-v1.6) — >32-bit uids round-trip (gossip, heartbeat, handshake PDU, DData; generation range tests in `AddressUidExtensionSpecs`)
- [x] 8.3 Add `BREAKING_CHANGES_V1.6.md` entry (API + Wire rows)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//-----------------------------------------------------------------------
// <copyright file="UniqueAddressSerializationSupportSpec.cs" company="Akka.NET Project">
// Copyright (C) 2009-2022 Lightbend Inc. <http://www.lightbend.com>
// Copyright (C) 2013-2025 .NET Foundation <https://github.com/akkadotnet/akka.net>
// </copyright>
//-----------------------------------------------------------------------

using Akka.Actor;
using Akka.Configuration;
using Akka.DistributedData.Serialization;
using FluentAssertions;
using Xunit;
using Address = Akka.Actor.Address;
using UniqueAddress = Akka.Cluster.UniqueAddress;

namespace Akka.DistributedData.Tests.Serialization
{
/// <summary>
/// Task 8.2: DData's <see cref="Proto.Msg.UniqueAddress"/> wire field was already <c>int64</c>
/// (ReplicatorMessages.proto) before the widen-system-uid-to-64bit change - only the C# narrowing cast in
/// <see cref="SerializationSupport.UniqueAddressFromProto"/> was removed. This verifies >32-bit uids
/// round-trip cleanly through <see cref="SerializationSupport.UniqueAddressToProto"/> /
/// <see cref="SerializationSupport.UniqueAddressFromProto"/>.
/// </summary>
[Collection("DistributedDataSpec")]
public class UniqueAddressSerializationSupportSpec : TestKit.Xunit.TestKit
{
private static readonly Config BaseConfig = ConfigurationFactory.ParseString(@"
akka.actor {
provider=""Akka.Cluster.ClusterActorRefProvider, Akka.Cluster""
}
akka.remote.dot-netty.tcp.port = 0").WithFallback(DistributedData.DefaultConfig());

private readonly SerializationSupport _support;

public UniqueAddressSerializationSupportSpec(ITestOutputHelper output)
: base(BaseConfig, "UniqueAddressSerializationSupportSpec", output)
{
_support = new SerializationSupport((ExtendedActorSystem)Sys);
}

[Theory(DisplayName = "Should_round_trip_unchanged_When_SerializationSupport_converts_a_64_bit_uid")]
[InlineData(12345L)]
[InlineData(long.MaxValue)]
[InlineData(unchecked((long)0x8000_0000_0000_0001))] // negative
[InlineData(1L << 40)]
public void Should_round_trip_unchanged_When_SerializationSupport_converts_a_64_bit_uid(long uid)
{
var address = new UniqueAddress(new Address("akka.tcp", Sys.Name, "some.host.org", 4711), uid);

var proto = SerializationSupport.UniqueAddressToProto(address);
var roundTripped = _support.UniqueAddressFromProto(proto);

roundTripped.Should().Be(address);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public static Proto.Msg.UniqueAddress UniqueAddressToProto(UniqueAddress address

public UniqueAddress UniqueAddressFromProto(Proto.Msg.UniqueAddress address)
{
return new UniqueAddress(AddressFromProto(address.Address), (int)address.Uid);
return new UniqueAddress(AddressFromProto(address.Address), address.Uid);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DData's UniqueAddress.uid in ReplicatorMessages.proto was already int64 before this change, so this is just the C# (int) narrowing coming off. UniqueAddressSerializationSupportSpec adds a >32-bit round-trip over UniqueAddressToProto / UniqueAddressFromProto to lock that in.

}

public static Proto.Msg.VersionVector VersionVectorToProto(VersionVector versionVector)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,9 @@ namespace Akka.Cluster
}
public class UniqueAddress : System.IComparable, System.IComparable<Akka.Cluster.UniqueAddress>, System.IEquatable<Akka.Cluster.UniqueAddress>
{
public UniqueAddress(Akka.Actor.Address address, int uid) { }
public UniqueAddress(Akka.Actor.Address address, long uid) { }
public Akka.Actor.Address Address { get; }
public int Uid { get; }
public long Uid { get; }
public int CompareTo(Akka.Cluster.UniqueAddress uniqueAddress) { }
public bool Equals(Akka.Cluster.UniqueAddress other) { }
public override bool Equals(object obj) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,9 @@ namespace Akka.Cluster
}
public class UniqueAddress : System.IComparable, System.IComparable<Akka.Cluster.UniqueAddress>, System.IEquatable<Akka.Cluster.UniqueAddress>
{
public UniqueAddress(Akka.Actor.Address address, int uid) { }
public UniqueAddress(Akka.Actor.Address address, long uid) { }
public Akka.Actor.Address Address { get; }
public int Uid { get; }
public long Uid { get; }
public int CompareTo(Akka.Cluster.UniqueAddress uniqueAddress) { }
public bool Equals(Akka.Cluster.UniqueAddress other) { }
public override bool Equals(object obj) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ namespace Akka.Remote
{
public class AddressUid : Akka.Actor.IExtension
{
public readonly int Uid;
public readonly long Uid;
public AddressUid() { }
}
public class AddressUidExtension : Akka.Actor.ExtensionIdProvider<Akka.Remote.AddressUid>
{
public AddressUidExtension() { }
public override Akka.Remote.AddressUid CreateExtension(Akka.Actor.ExtendedActorSystem system) { }
public static int Uid(Akka.Actor.ActorSystem system) { }
public static long Uid(Akka.Actor.ActorSystem system) { }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fastest way to check the public-surface blast radius. The approved-file delta is only the uid members: AddressUid.Uid, AddressUidExtension.Uid, the three Quarantine(Address, long?) overloads plus RemoteWatcher.Quarantine, QuarantinedEvent ctor/Uid, HeartbeatRsp ctor/AddressUid, and on the Cluster side UniqueAddress ctor/Uid. Nothing else shifted, so no unrelated API drift rode along. All four verified files (DotNet/Net x Remote/Cluster) carry the same int->long delta.

}
public class AkkaProtocolSettings
{
Expand Down Expand Up @@ -143,7 +143,7 @@ namespace Akka.Remote
bool HasAddress(Akka.Actor.Address address);
Akka.Actor.IActorRef InternalResolveActorRef(string path);
Akka.Actor.Deploy LookUpRemotes(System.Collections.Generic.IEnumerable<string> p);
void Quarantine(Akka.Actor.Address address, int? uid);
void Quarantine(Akka.Actor.Address address, long? uid);
Akka.Actor.IInternalActorRef ResolveActorRefWithLocalAddress(string path, Akka.Actor.Address localAddress);
void UseActorOnNode(Akka.Remote.RemoteActorRef actor, Akka.Actor.Props props, Akka.Actor.Deploy deploy, Akka.Actor.IInternalActorRef supervisor);
}
Expand All @@ -161,9 +161,9 @@ namespace Akka.Remote
}
public sealed class QuarantinedEvent : Akka.Remote.RemotingLifecycleEvent
{
public QuarantinedEvent(Akka.Actor.Address address, int uid) { }
public QuarantinedEvent(Akka.Actor.Address address, long uid) { }
public Akka.Actor.Address Address { get; }
public int Uid { get; }
public long Uid { get; }
public override Akka.Event.LogLevel LogLevel() { }
public override string ToString() { }
}
Expand Down Expand Up @@ -219,7 +219,7 @@ namespace Akka.Remote
public virtual void Init(Akka.Actor.Internal.ActorSystemImpl system) { }
public Akka.Actor.IActorRef InternalResolveActorRef(string path) { }
public Akka.Actor.Deploy LookUpRemotes(System.Collections.Generic.IEnumerable<string> p) { }
public void Quarantine(Akka.Actor.Address address, int? uid) { }
public void Quarantine(Akka.Actor.Address address, long? uid) { }
public void RegisterTempActor(Akka.Actor.IInternalActorRef actorRef, Akka.Actor.ActorPath path) { }
public Akka.Actor.IActorRef ResolveActorRef(Akka.Actor.ActorPath actorPath) { }
public Akka.Actor.IActorRef ResolveActorRef(string path) { }
Expand Down Expand Up @@ -286,7 +286,7 @@ namespace Akka.Remote
public abstract Akka.Actor.Address LocalAddressForRemote(Akka.Actor.Address remote);
public abstract System.Threading.Tasks.Task<bool> ManagementCommand(object cmd);
public abstract System.Threading.Tasks.Task<bool> ManagementCommand(object cmd, System.Threading.CancellationToken cancellationToken);
public abstract void Quarantine(Akka.Actor.Address address, int? uid);
public abstract void Quarantine(Akka.Actor.Address address, long? uid);
public abstract void Send(object message, Akka.Actor.IActorRef sender, Akka.Remote.RemoteActorRef recipient);
public abstract System.Threading.Tasks.Task Shutdown();
public abstract void Start();
Expand All @@ -308,7 +308,7 @@ namespace Akka.Remote
protected override void OnReceive(object message) { }
protected override void PostStop() { }
protected virtual void PublishAddressTerminated(Akka.Actor.Address address) { }
protected virtual void Quarantine(Akka.Actor.Address address, int? addressUid) { }
protected virtual void Quarantine(Akka.Actor.Address address, long? addressUid) { }
protected void RemoveWatch(Akka.Actor.IInternalActorRef watchee, Akka.Actor.IInternalActorRef watcher) { }
protected void RemoveWatchee(Akka.Actor.IInternalActorRef watchee) { }
protected void UnwatchNode(Akka.Actor.Address watcheeAddress) { }
Expand All @@ -325,8 +325,8 @@ namespace Akka.Remote
}
public class HeartbeatRsp
{
public HeartbeatRsp(int addressUid) { }
public int AddressUid { get; }
public HeartbeatRsp(long addressUid) { }
public long AddressUid { get; }
}
public class HeartbeatTick
{
Expand Down
Loading
Loading