From 16ed9a17c986d408bcc211ed3115eddb57c951a9 Mon Sep 17 00:00:00 2001 From: Tochemey Date: Fri, 21 Feb 2025 18:06:04 +0000 Subject: [PATCH] feat: add relocation flag toggling --- actor/actor_system.go | 9 ++- actor/actor_system_test.go | 8 ++- actor/cluster_singleton.go | 7 +- actor/pid.go | 18 ++--- actor/pid_option.go | 2 +- actor/pid_option_test.go | 4 +- actor/rebalancer.go | 12 +++- actor/remoting.go | 16 +++-- actor/remoting_test.go | 84 ++++++++++++++++++++++-- actor/spawn_option.go | 12 ++-- actor/spawn_option_test.go | 2 +- client/actor.go | 1 + client/client.go | 21 ++++-- client/client_test.go | 16 ++--- internal/cluster/engine.go | 1 + internal/internalpb/remoting.pb.go | 101 ++++++++++++++++------------- protos/internal/remoting.proto | 2 + remote/spawn_request.go | 76 ++++++++++++++++++++++ 18 files changed, 304 insertions(+), 88 deletions(-) create mode 100644 remote/spawn_request.go diff --git a/actor/actor_system.go b/actor/actor_system.go index 5e2f8ab2..22017ea5 100644 --- a/actor/actor_system.go +++ b/actor/actor_system.go @@ -1268,7 +1268,12 @@ func (x *actorSystem) RemoteSpawn(ctx context.Context, request *connect.Request[ } } - if _, err = x.Spawn(ctx, msg.GetActorName(), actor); err != nil { + var opts []SpawnOption + if !msg.GetRelocatable() { + opts = append(opts, WithRelocationDisabled()) + } + + if _, err = x.Spawn(ctx, msg.GetActorName(), actor, opts...); err != nil { logger.Errorf("failed to create actor=(%s) on [host=%s, port=%d]: reason: (%v)", msg.GetActorName(), msg.GetHost(), msg.GetPort(), err) return nil, connect.NewError(connect.CodeInternal, err) } @@ -1862,7 +1867,7 @@ func (x *actorSystem) configPID(ctx context.Context, name string, actor Actor, o pidOpts = append(pidOpts, asSingleton()) } - if spawnConfig.disableRelocation { + if !spawnConfig.relocatable { pidOpts = append(pidOpts, withRelocationDisabled()) } diff --git a/actor/actor_system_test.go b/actor/actor_system_test.go index d497757a..f65f6d32 100644 --- a/actor/actor_system_test.go +++ b/actor/actor_system_test.go @@ -1522,7 +1522,13 @@ func TestActorSystem(t *testing.T) { require.Nil(t, addr) // spawn the remote actor - err = remoting.RemoteSpawn(ctx, host, remotingPort, actorName, "actors.exchanger", false) + request := &remote.SpawnRequest{ + Name: actorName, + Kind: "actors.exchanger", + Singleton: false, + Relocatable: true, + } + err = remoting.RemoteSpawn(ctx, host, remotingPort, request) require.NoError(t, err) // re-fetching the address of the actor should return not nil address after start diff --git a/actor/cluster_singleton.go b/actor/cluster_singleton.go index 55615602..67ed0aed 100644 --- a/actor/cluster_singleton.go +++ b/actor/cluster_singleton.go @@ -38,6 +38,7 @@ import ( "github.com/tochemey/goakt/v3/internal/internalpb" "github.com/tochemey/goakt/v3/internal/types" "github.com/tochemey/goakt/v3/log" + "github.com/tochemey/goakt/v3/remote" ) // clusterSingletonManager is a system actor that manages the lifecycle of singleton actors @@ -162,5 +163,9 @@ func (x *actorSystem) spawnSingletonOnLeader(ctx context.Context, cl cluster.Int port = int(peerState.GetRemotingPort()) ) - return x.remoting.RemoteSpawn(ctx, host, port, name, actorType, true) + return x.remoting.RemoteSpawn(ctx, host, port, &remote.SpawnRequest{ + Name: name, + Kind: actorType, + Singleton: true, + }) } diff --git a/actor/pid.go b/actor/pid.go index c870d063..20ffa15e 100644 --- a/actor/pid.go +++ b/actor/pid.go @@ -143,10 +143,10 @@ type PID struct { remoting *Remoting - goScheduler *goScheduler - startedAt *atomic.Int64 - isSingleton atomic.Bool - disableRelocation atomic.Bool + goScheduler *goScheduler + startedAt *atomic.Int64 + isSingleton atomic.Bool + relocatable atomic.Bool } // newPID creates a new pid @@ -193,7 +193,7 @@ func newPID(ctx context.Context, address *address.Address, actor Actor, opts ... pid.passivateAfter.Store(DefaultPassivationTimeout) pid.initTimeout.Store(DefaultInitTimeout) pid.processing.Store(int32(idle)) - pid.disableRelocation.Store(false) + pid.relocatable.Store(true) for _, opt := range opts { opt(pid) @@ -392,13 +392,13 @@ func (pid *PID) IsSingleton() bool { return pid.isSingleton.Load() } -// IsRelocatable determines whether the actor can be relocated to another node if its host node shuts down unexpectedly. +// IsRelocatable determines whether the actor can be relocated to another node when its host node shuts down unexpectedly. // By default, actors are relocatable to ensure system resilience and high availability. // However, this behavior can be disabled during the actor's creation using the WithRelocationDisabled option. // // Returns true if relocation is allowed, and false if relocation is disabled. func (pid *PID) IsRelocatable() bool { - return !pid.disableRelocation.Load() + return pid.relocatable.Load() } // ActorSystem returns the actor system @@ -585,7 +585,7 @@ func (pid *PID) SpawnChild(ctx context.Context, name string, actor Actor, opts . } // set the relocation flag - if spawnConfig.disableRelocation { + if spawnConfig.relocatable { pidOptions = append(pidOptions, withRelocationDisabled()) } @@ -1357,7 +1357,7 @@ func (pid *PID) reset() { pid.supervisor.Reset() pid.mailbox.Dispose() pid.isSingleton.Store(false) - pid.disableRelocation.Store(false) + pid.relocatable.Store(true) } // freeWatchers releases all the actors watching this actor diff --git a/actor/pid_option.go b/actor/pid_option.go index fb6b544d..c352633e 100644 --- a/actor/pid_option.go +++ b/actor/pid_option.go @@ -121,6 +121,6 @@ func asSingleton() pidOption { // withRelocationDisabled disables the actor relocation func withRelocationDisabled() pidOption { return func(pid *PID) { - pid.disableRelocation.Store(true) + pid.relocatable.Store(false) } } diff --git a/actor/pid_option_test.go b/actor/pid_option_test.go index 53e5d5ce..c97dde87 100644 --- a/actor/pid_option_test.go +++ b/actor/pid_option_test.go @@ -44,6 +44,7 @@ func TestPIDOptions(t *testing.T) { negativeDuration atomic.Duration atomicUint64 atomic.Uint64 atomicTrue atomic.Bool + atomicFalse atomic.Bool ) negativeDuration.Store(-1) atomicInt.Store(5) @@ -51,6 +52,7 @@ func TestPIDOptions(t *testing.T) { atomicUint64.Store(10) eventsStream := eventstream.New() atomicTrue.Store(true) + atomicFalse.Store(false) testCases := []struct { name string @@ -107,7 +109,7 @@ func TestPIDOptions(t *testing.T) { { name: "withRelocationDisabled", option: withRelocationDisabled(), - expected: &PID{disableRelocation: atomicTrue}, + expected: &PID{relocatable: atomicFalse}, }, } for _, tc := range testCases { diff --git a/actor/rebalancer.go b/actor/rebalancer.go index 436ba6ab..d70cb1fa 100644 --- a/actor/rebalancer.go +++ b/actor/rebalancer.go @@ -36,6 +36,7 @@ import ( "github.com/tochemey/goakt/v3/internal/collection/slice" "github.com/tochemey/goakt/v3/internal/internalpb" "github.com/tochemey/goakt/v3/log" + "github.com/tochemey/goakt/v3/remote" ) // rebalancer is a system actor that helps rebalance cluster @@ -134,10 +135,15 @@ func (r *rebalancer) Rebalance(ctx *ReceiveContext) { if actor.GetRelocatable() { host := peerState.GetHost() port := int(peerState.GetRemotingPort()) - actorName := actor.GetActorName() - actorType := actor.GetActorType() - if err := r.remoting.RemoteSpawn(egCtx, host, port, actorName, actorType, false); err != nil { + spawnRequest := &remote.SpawnRequest{ + Name: actor.GetActorName(), + Kind: actor.GetActorType(), + Singleton: false, + Relocatable: true, + } + + if err := r.remoting.RemoteSpawn(egCtx, host, port, spawnRequest); err != nil { logger.Error(err) return NewSpawnError(err) } diff --git a/actor/remoting.go b/actor/remoting.go index e2b246d7..13122d45 100644 --- a/actor/remoting.go +++ b/actor/remoting.go @@ -28,6 +28,7 @@ import ( "context" "crypto/tls" "errors" + "fmt" nethttp "net/http" "strings" "time" @@ -41,6 +42,7 @@ import ( "github.com/tochemey/goakt/v3/internal/http" "github.com/tochemey/goakt/v3/internal/internalpb" "github.com/tochemey/goakt/v3/internal/internalpb/internalpbconnect" + "github.com/tochemey/goakt/v3/remote" ) // RemotingOption sets the remoting option @@ -315,15 +317,21 @@ func (r *Remoting) RemoteBatchAsk(ctx context.Context, from, to *address.Address } // RemoteSpawn creates an actor on a remote node. The given actor needs to be registered on the remote node using the Register method of ActorSystem -func (r *Remoting) RemoteSpawn(ctx context.Context, host string, port int, name, actorType string, singleton bool) error { +func (r *Remoting) RemoteSpawn(ctx context.Context, host string, port int, spawnRequest *remote.SpawnRequest) error { + if err := spawnRequest.Validate(); err != nil { + return fmt.Errorf("invalid spawn option: %w", err) + } + + spawnRequest.Sanitize() remoteClient := r.serviceClient(host, port) request := connect.NewRequest( &internalpb.RemoteSpawnRequest{ Host: host, Port: int32(port), - ActorName: name, - ActorType: actorType, - IsSingleton: singleton, + ActorName: spawnRequest.Name, + ActorType: spawnRequest.Kind, + IsSingleton: spawnRequest.Singleton, + Relocatable: spawnRequest.Relocatable, }, ) diff --git a/actor/remoting_test.go b/actor/remoting_test.go index d8a4bf8b..a426e4ff 100644 --- a/actor/remoting_test.go +++ b/actor/remoting_test.go @@ -1696,7 +1696,13 @@ func TestRemotingSpawn(t *testing.T) { require.NoError(t, err) // spawn the remote actor - err = remoting.RemoteSpawn(ctx, host, remotingPort, actorName, "actors.exchanger", false) + request := &remote.SpawnRequest{ + Name: actorName, + Kind: "actors.exchanger", + Singleton: false, + Relocatable: false, + } + err = remoting.RemoteSpawn(ctx, host, remotingPort, request) require.NoError(t, err) // re-fetching the address of the actor should return not nil address after start @@ -1761,7 +1767,13 @@ func TestRemotingSpawn(t *testing.T) { require.Nil(t, addr) // spawn the remote actor - err = remoting.RemoteSpawn(ctx, sys.Host(), int(sys.Port()), actorName, "actors.exchanger", false) + request := &remote.SpawnRequest{ + Name: actorName, + Kind: "actors.exchanger", + Singleton: false, + Relocatable: false, + } + err = remoting.RemoteSpawn(ctx, sys.Host(), int(sys.Port()), request) require.Error(t, err) assert.EqualError(t, err, ErrTypeNotRegistered.Error()) @@ -1799,7 +1811,13 @@ func TestRemotingSpawn(t *testing.T) { actorName := uuid.NewString() remoting := NewRemoting() // spawn the remote actor - err = remoting.RemoteSpawn(ctx, host, remotingPort, actorName, "actors.exchanger", false) + request := &remote.SpawnRequest{ + Name: actorName, + Kind: "actors.exchanger", + Singleton: false, + Relocatable: false, + } + err = remoting.RemoteSpawn(ctx, host, remotingPort, request) require.Error(t, err) t.Cleanup( @@ -1862,7 +1880,13 @@ func TestRemotingSpawn(t *testing.T) { require.NoError(t, err) // spawn the remote actor - err = remoting.RemoteSpawn(ctx, host, remotingPort, actorName, "actors.exchanger", false) + request := &remote.SpawnRequest{ + Name: actorName, + Kind: "actors.exchanger", + Singleton: false, + Relocatable: false, + } + err = remoting.RemoteSpawn(ctx, host, remotingPort, request) require.NoError(t, err) // re-fetching the address of the actor should return not nil address after start @@ -1893,4 +1917,56 @@ func TestRemotingSpawn(t *testing.T) { }, ) }) + t.Run("When request is invalid", func(t *testing.T) { + // create the context + ctx := context.TODO() + // define the logger to use + logger := log.DiscardLogger + // generate the remoting port + ports := dynaport.Get(1) + remotingPort := ports[0] + host := "127.0.0.1" + + // create the actor system + sys, err := NewActorSystem( + "test", + WithLogger(logger), + WithPassivationDisabled(), + WithRemote(remote.NewConfig(host, remotingPort)), + ) + // assert there are no error + require.NoError(t, err) + + // start the actor system + err = sys.Start(ctx) + assert.NoError(t, err) + + // create an actor implementation and register it + actor := &exchanger{} + actorName := uuid.NewString() + + remoting := NewRemoting() + // fetching the address of the that actor should return nil address + addr, err := remoting.RemoteLookup(ctx, sys.Host(), int(sys.Port()), actorName) + require.NoError(t, err) + require.Nil(t, addr) + + // register the actor + err = sys.Register(ctx, actor) + require.NoError(t, err) + + // spawn the remote actor + request := &remote.SpawnRequest{ + Name: "", + Kind: "actors.exchanger", + Singleton: false, + Relocatable: false, + } + err = remoting.RemoteSpawn(ctx, host, remotingPort, request) + require.Error(t, err) + + remoting.Close() + err = sys.Stop(ctx) + assert.NoError(t, err) + }) } diff --git a/actor/spawn_option.go b/actor/spawn_option.go index 9b152b5a..aae5a091 100644 --- a/actor/spawn_option.go +++ b/actor/spawn_option.go @@ -40,13 +40,17 @@ type spawnConfig struct { passivateAfter *time.Duration // specifies if the actor is a singleton asSingleton bool - // specifies if the actor should be rebalanced - disableRelocation bool + // specifies if the actor should be relocated + relocatable bool } // newSpawnConfig creates an instance of spawnConfig func newSpawnConfig(opts ...SpawnOption) *spawnConfig { - config := new(spawnConfig) + config := &spawnConfig{ + relocatable: true, + asSingleton: false, + } + for _, opt := range opts { opt.Apply(config) } @@ -124,7 +128,7 @@ func WithLongLived() SpawnOption { // cannot be easily replicated. func WithRelocationDisabled() SpawnOption { return spawnOption(func(config *spawnConfig) { - config.disableRelocation = true + config.relocatable = false }) } diff --git a/actor/spawn_option_test.go b/actor/spawn_option_test.go index f7c6bb3a..3d2a21ee 100644 --- a/actor/spawn_option_test.go +++ b/actor/spawn_option_test.go @@ -69,7 +69,7 @@ func TestSpawnOption(t *testing.T) { config := &spawnConfig{} option := WithRelocationDisabled() option.Apply(config) - require.Equal(t, &spawnConfig{disableRelocation: true}, config) + require.Equal(t, &spawnConfig{relocatable: true}, config) }) } diff --git a/client/actor.go b/client/actor.go index bb3592b8..6d391b1b 100644 --- a/client/actor.go +++ b/client/actor.go @@ -28,6 +28,7 @@ package client type Actor struct { name string // Name defines the actor name. This will be unique in the Client kind string // Kind specifies the actor kind. + } // NewActor creates an instance of Actor diff --git a/client/client.go b/client/client.go index 8e8f2a59..d4b565e0 100644 --- a/client/client.go +++ b/client/client.go @@ -42,6 +42,7 @@ import ( "github.com/tochemey/goakt/v3/internal/ticker" "github.com/tochemey/goakt/v3/internal/types" "github.com/tochemey/goakt/v3/internal/validation" + "github.com/tochemey/goakt/v3/remote" ) // Client connects to af Go-Akt nodes. @@ -133,23 +134,35 @@ func (x *Client) Kinds(ctx context.Context) ([]string, error) { } // Spawn creates an actor provided the actor name. -func (x *Client) Spawn(ctx context.Context, actor *Actor, singleton bool) (err error) { +func (x *Client) Spawn(ctx context.Context, actor *Actor, singleton, relocatable bool) (err error) { x.locker.Lock() node := nextNode(x.balancer) x.locker.Unlock() remoteHost, remotePort := node.HostAndPort() - return node.Remoting().RemoteSpawn(ctx, remoteHost, remotePort, actor.Name(), actor.Kind(), singleton) + spawnRequest := &remote.SpawnRequest{ + Name: actor.Name(), + Kind: actor.Kind(), + Singleton: singleton, + Relocatable: relocatable, + } + return node.Remoting().RemoteSpawn(ctx, remoteHost, remotePort, spawnRequest) } // SpawnWithBalancer creates an actor provided the actor name and the balancer strategy -func (x *Client) SpawnWithBalancer(ctx context.Context, actor *Actor, strategy BalancerStrategy) (err error) { +func (x *Client) SpawnWithBalancer(ctx context.Context, actor *Actor, singleton, relocatable bool, strategy BalancerStrategy) (err error) { x.locker.Lock() balancer := getBalancer(strategy) balancer.Set(x.nodes...) node := nextNode(balancer) remoteHost, remotePort := node.HostAndPort() x.locker.Unlock() - return node.Remoting().RemoteSpawn(ctx, remoteHost, remotePort, actor.Name(), actor.Kind(), false) + spawnRequest := &remote.SpawnRequest{ + Name: actor.Name(), + Kind: actor.Kind(), + Singleton: singleton, + Relocatable: relocatable, + } + return node.Remoting().RemoteSpawn(ctx, remoteHost, remotePort, spawnRequest) } // ReSpawn restarts a given actor diff --git a/client/client_test.go b/client/client_test.go index a9262c13..2b4921c0 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -93,7 +93,7 @@ func TestClient(t *testing.T) { require.ElementsMatch(t, expected, kinds) actor := NewActor("client.testactor").WithName("actorName") - err = client.Spawn(ctx, actor, false) + err = client.Spawn(ctx, actor, false, true) require.NoError(t, err) util.Pause(time.Second) @@ -177,7 +177,7 @@ func TestClient(t *testing.T) { require.ElementsMatch(t, expected, kinds) actor := NewActor("client.testactor").WithName("actorName") - err = client.Spawn(ctx, actor, false) + err = client.Spawn(ctx, actor, false, true) require.NoError(t, err) util.Pause(time.Second) @@ -263,7 +263,7 @@ func TestClient(t *testing.T) { require.ElementsMatch(t, expected, kinds) actor := NewActor("client.testactor").WithName("actorName") - err = client.Spawn(ctx, actor, false) + err = client.Spawn(ctx, actor, false, true) require.NoError(t, err) util.Pause(time.Second) @@ -345,7 +345,7 @@ func TestClient(t *testing.T) { require.ElementsMatch(t, expected, kinds) actor := NewActor("client.testactor").WithName("actorName") - err = client.Spawn(ctx, actor, false) + err = client.Spawn(ctx, actor, false, true) require.NoError(t, err) util.Pause(time.Second) @@ -428,7 +428,7 @@ func TestClient(t *testing.T) { require.ElementsMatch(t, expected, kinds) actor := NewActor("client.testactor").WithName("actorName") - err = client.SpawnWithBalancer(ctx, actor, RandomStrategy) + err = client.SpawnWithBalancer(ctx, actor, false, true, RandomStrategy) require.NoError(t, err) util.Pause(time.Second) @@ -510,7 +510,7 @@ func TestClient(t *testing.T) { require.ElementsMatch(t, expected, kinds) actor := NewActor("client.testactor").WithName("actorName") - err = client.Spawn(ctx, actor, false) + err = client.Spawn(ctx, actor, false, true) require.NoError(t, err) util.Pause(time.Second) @@ -597,7 +597,7 @@ func TestClient(t *testing.T) { require.ElementsMatch(t, expected, kinds) actor := NewActor("client.testactor").WithName("actorName") - err = client.Spawn(ctx, actor, false) + err = client.Spawn(ctx, actor, false, true) require.NoError(t, err) util.Pause(time.Second) @@ -684,7 +684,7 @@ func TestClient(t *testing.T) { require.ElementsMatch(t, expected, kinds) actor := NewActor("client.testactor").WithName("actorName") - err = client.Spawn(ctx, actor, false) + err = client.Spawn(ctx, actor, false, true) require.NoError(t, err) util.Pause(time.Second) diff --git a/internal/cluster/engine.go b/internal/cluster/engine.go index d50927e6..1e6521ab 100644 --- a/internal/cluster/engine.go +++ b/internal/cluster/engine.go @@ -538,6 +538,7 @@ func (x *Engine) PutActor(ctx context.Context, actor *internalpb.ActorRef) error ActorName: actorName, ActorType: actor.GetActorType(), IsSingleton: actor.GetIsSingleton(), + Relocatable: actor.GetRelocatable(), } x.peerState.Actors = actors diff --git a/internal/internalpb/remoting.pb.go b/internal/internalpb/remoting.pb.go index 75faece2..013ac4db 100644 --- a/internal/internalpb/remoting.pb.go +++ b/internal/internalpb/remoting.pb.go @@ -591,7 +591,9 @@ type RemoteSpawnRequest struct { // Specifies the actor type ActorType string `protobuf:"bytes,4,opt,name=actor_type,json=actorType,proto3" json:"actor_type,omitempty"` // Specifies if the actor is a singleton - IsSingleton bool `protobuf:"varint,5,opt,name=is_singleton,json=isSingleton,proto3" json:"is_singleton,omitempty"` + IsSingleton bool `protobuf:"varint,5,opt,name=is_singleton,json=isSingleton,proto3" json:"is_singleton,omitempty"` + // Specifies if the actor is relocatable + Relocatable bool `protobuf:"varint,6,opt,name=relocatable,proto3" json:"relocatable,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -661,6 +663,13 @@ func (x *RemoteSpawnRequest) GetIsSingleton() bool { return false } +func (x *RemoteSpawnRequest) GetRelocatable() bool { + if x != nil { + return x.Relocatable + } + return false +} + type RemoteSpawnResponse struct { state protoimpl.MessageState `protogen:"open.v1"` unknownFields protoimpl.UnknownFields @@ -795,7 +804,7 @@ var file_internal_remoting_proto_rawDesc = string([]byte{ 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x14, 0x0a, 0x12, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9d, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x6d, 0x6f, 0x74, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xbf, 0x01, 0x0a, 0x12, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, 0x70, 0x61, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x6f, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, @@ -805,52 +814,54 @@ var file_internal_remoting_proto_rawDesc = string([]byte{ 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x69, 0x73, 0x5f, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x74, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x69, 0x73, 0x53, 0x69, 0x6e, - 0x67, 0x6c, 0x65, 0x74, 0x6f, 0x6e, 0x22, 0x15, 0x0a, 0x13, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, - 0x53, 0x70, 0x61, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x0d, 0x0a, - 0x0b, 0x53, 0x70, 0x61, 0x77, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0xf4, 0x03, 0x0a, - 0x0f, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x4c, 0x0a, 0x09, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x73, 0x6b, 0x12, 0x1c, 0x2e, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, - 0x65, 0x41, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41, - 0x73, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x12, 0x4d, - 0x0a, 0x0a, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x54, 0x65, 0x6c, 0x6c, 0x12, 0x1d, 0x2e, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, - 0x54, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x54, - 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x12, 0x51, 0x0a, - 0x0c, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x12, 0x1f, 0x2e, + 0x67, 0x6c, 0x65, 0x74, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x72, 0x65, 0x6c, 0x6f, 0x63, 0x61, + 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x72, 0x65, 0x6c, + 0x6f, 0x63, 0x61, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x22, 0x15, 0x0a, 0x13, 0x52, 0x65, 0x6d, 0x6f, + 0x74, 0x65, 0x53, 0x70, 0x61, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x0d, 0x0a, 0x0b, 0x53, 0x70, 0x61, 0x77, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x32, 0xf4, + 0x03, 0x0a, 0x0f, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x12, 0x4c, 0x0a, 0x09, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x41, 0x73, 0x6b, 0x12, + 0x1c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, + 0x6f, 0x74, 0x65, 0x41, 0x73, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1d, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, - 0x65, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, + 0x65, 0x41, 0x73, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, + 0x12, 0x4d, 0x0a, 0x0a, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x54, 0x65, 0x6c, 0x6c, 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, - 0x74, 0x65, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x54, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x53, 0x70, 0x61, 0x77, - 0x6e, 0x12, 0x20, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x2e, 0x52, - 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x53, 0x70, 0x61, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, + 0x74, 0x65, 0x54, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, + 0x65, 0x54, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x28, 0x01, 0x12, + 0x51, 0x0a, 0x0c, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x12, + 0x1f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, + 0x6f, 0x74, 0x65, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x20, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x2e, 0x52, 0x65, + 0x6d, 0x6f, 0x74, 0x65, 0x4c, 0x6f, 0x6f, 0x6b, 0x75, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x54, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x53, 0x70, + 0x61, 0x77, 0x6e, 0x12, 0x20, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x53, 0x70, 0x61, 0x77, 0x6e, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0a, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, - 0x53, 0x74, 0x6f, 0x70, 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, - 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, - 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0b, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, 0x70, 0x61, - 0x77, 0x6e, 0x12, 0x1e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x2e, - 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, 0x70, 0x61, 0x77, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x2e, - 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, 0x70, 0x61, 0x77, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x42, 0xa6, 0x01, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x2e, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x42, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x69, 0x6e, 0x67, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x48, 0x02, 0x50, 0x01, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x6f, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x79, 0x2f, 0x67, - 0x6f, 0x61, 0x6b, 0x74, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, - 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x3b, 0x69, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0xa2, 0x02, 0x03, 0x49, 0x58, 0x58, 0xaa, 0x02, 0x0a, 0x49, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0xca, 0x02, 0x0a, 0x49, 0x6e, 0x74, 0x65, - 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0xe2, 0x02, 0x16, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x70, 0x62, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, - 0x02, 0x0a, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x53, 0x70, 0x61, 0x77, 0x6e, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4b, 0x0a, 0x0a, 0x52, 0x65, 0x6d, 0x6f, + 0x74, 0x65, 0x53, 0x74, 0x6f, 0x70, 0x12, 0x1d, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x70, 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, 0x74, 0x6f, 0x70, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x0b, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, + 0x70, 0x61, 0x77, 0x6e, 0x12, 0x1e, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, + 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, 0x70, 0x61, 0x77, 0x6e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1f, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, + 0x62, 0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x53, 0x70, 0x61, 0x77, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0xa6, 0x01, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x42, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x69, + 0x6e, 0x67, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x48, 0x02, 0x50, 0x01, 0x5a, 0x3b, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x74, 0x6f, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x79, + 0x2f, 0x67, 0x6f, 0x61, 0x6b, 0x74, 0x2f, 0x76, 0x33, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x3b, 0x69, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0xa2, 0x02, 0x03, 0x49, 0x58, 0x58, 0xaa, 0x02, + 0x0a, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0xca, 0x02, 0x0a, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0xe2, 0x02, 0x16, 0x49, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0xea, 0x02, 0x0a, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/protos/internal/remoting.proto b/protos/internal/remoting.proto index 5e6b19bc..548835e9 100644 --- a/protos/internal/remoting.proto +++ b/protos/internal/remoting.proto @@ -113,6 +113,8 @@ message RemoteSpawnRequest { string actor_type = 4; // Specifies if the actor is a singleton bool is_singleton = 5; + // Specifies if the actor is relocatable + bool relocatable = 6; } message RemoteSpawnResponse {} diff --git a/remote/spawn_request.go b/remote/spawn_request.go new file mode 100644 index 00000000..bb044094 --- /dev/null +++ b/remote/spawn_request.go @@ -0,0 +1,76 @@ +/* + * MIT License + * + * Copyright (c) 2022-2025 Arsene Tochemey Gandote + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package remote + +import ( + "strings" + + "github.com/tochemey/goakt/v3/internal/validation" +) + +// SpawnRequest defines configuration options for spawning an actor on a remote node. +// These options control the actor’s identity, behavior, and lifecycle, especially in scenarios involving node failures or load balancing. +type SpawnRequest struct { + // Name represents the unique name of the actor. + // This name is used to identify and reference the actor across different nodes. + Name string + + // Kind represents the type of the actor. + // It typically corresponds to the actor’s implementation within the system + Kind string + + // Singleton specifies whether the actor is a singleton, meaning only one instance of the actor + // can exist across the entire cluster at any given time. + // This option is useful for actors responsible for global coordination or shared state. + // When Singleton is set to true it means that the given actor is automatically relocatable + Singleton bool + + // Relocatable indicates whether the actor can be automatically relocated to another node + // if its current host node unexpectedly shuts down. + // By default, actors are relocatable to ensure system resilience and high availability. + // Setting this to false ensures that the actor will not be redeployed after a node failure, + // which may be necessary for actors with node-specific dependencies or state. + Relocatable bool +} + +var _ validation.Validator = (*SpawnRequest)(nil) + +// Validate validates the SpawnRequest +func (s *SpawnRequest) Validate() error { + return validation. + New(validation.FailFast()). + AddValidator(validation.NewEmptyStringValidator("Name", s.Name)). + AddValidator(validation.NewEmptyStringValidator("Kind", s.Kind)). + Validate() +} + +// Sanitize sanitizes the request +func (s *SpawnRequest) Sanitize() { + s.Name = strings.TrimSpace(s.Name) + s.Kind = strings.TrimSpace(s.Kind) + if s.Singleton { + s.Relocatable = true + } +}