diff --git a/README.md b/README.md index 4f8d86b6..e3f3a369 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,10 @@ The complete Documentation can be found [here](https://tochemey.gitbook.io/goakt Kindly check out the [examples](https://github.com/Tochemey/goakt-examples)' repository. +## 💪 Support + +GoAkt is free and open source. If you need priority support on complex topics or request new features, please consider [sponsorship](https://github.com/sponsors/Tochemey). + ## 🌍 Community You can join these groups and chat to discuss and ask GoAkt related questions on: diff --git a/actor/actor_ref.go b/actor/actor_ref.go index d588752a..e6448c47 100644 --- a/actor/actor_ref.go +++ b/actor/actor_ref.go @@ -54,6 +54,7 @@ type ActorRef struct { address *address.Address // isSingleton defines if the actor is a singleton isSingleton bool + relocatable bool } // Name represents the actor given name @@ -77,6 +78,15 @@ func (x ActorRef) IsSingleton() bool { return x.isSingleton } +// IsRelocatable determines whether the actor can be relocated to another node if 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 (x ActorRef) IsRelocatable() bool { + return x.relocatable +} + // Equals is a convenient method to compare two ActorRef func (x ActorRef) Equals(actor ActorRef) bool { return x.address.Equals(actor.address) @@ -88,6 +98,7 @@ func fromActorRef(actorRef *internalpb.ActorRef) ActorRef { kind: actorRef.GetActorType(), address: address.From(actorRef.GetActorAddress()), isSingleton: actorRef.GetIsSingleton(), + relocatable: actorRef.GetRelocatable(), } } @@ -97,5 +108,6 @@ func fromPID(pid *PID) ActorRef { kind: types.Name(pid.Actor()), address: pid.Address(), isSingleton: pid.IsSingleton(), + relocatable: pid.IsRelocatable(), } } diff --git a/actor/actor_system.go b/actor/actor_system.go index 14530d1d..22017ea5 100644 --- a/actor/actor_system.go +++ b/actor/actor_system.go @@ -198,7 +198,7 @@ type ActorSystem interface { // handleRemoteTell handles an asynchronous message to an actor handleRemoteTell(ctx context.Context, to *PID, message proto.Message) error // broadcastActor sets actor in the actor system actors registry - broadcastActor(actor *PID, singleton bool) + broadcastActor(actor *PID) // getPeerStateFromStore returns the peer state from the cluster store getPeerStateFromStore(address string) (*internalpb.PeerState, error) // removePeerStateFromStore removes the peer state from the cluster store @@ -671,7 +671,7 @@ func (x *actorSystem) Spawn(ctx context.Context, name string, actor Actor, opts guardian := x.getUserGuardian() _ = x.actors.AddNode(guardian, pid) x.actors.AddWatcher(pid, x.deathWatch) - x.broadcastActor(pid, false) + x.broadcastActor(pid) return pid, nil } @@ -707,7 +707,7 @@ func (x *actorSystem) SpawnNamedFromFunc(ctx context.Context, name string, recei x.actorsCounter.Inc() _ = x.actors.AddNode(x.userGuardian, pid) x.actors.AddWatcher(pid, x.deathWatch) - x.broadcastActor(pid, false) + x.broadcastActor(pid) return pid, nil } @@ -731,9 +731,10 @@ func (x *actorSystem) SpawnRouter(ctx context.Context, poolSize int, routeesKind // A singleton actor like any other actor is created only once within the system and in the cluster. // A singleton actor is created with the default supervisor strategy and directive. // A singleton actor once created lives throughout the lifetime of the given actor system. +// One cannot create a child actor for a singleton actor. // // The cluster singleton is automatically started on the oldest node in the cluster. -// If the oldest node leaves the cluster, the singleton is restarted on the new oldest node. +// When the oldest node leaves the cluster unexpectedly, the singleton is restarted on the new oldest node. // This is useful for managing shared resources or coordinating tasks that should be handled by a single actor. func (x *actorSystem) SpawnSingleton(ctx context.Context, name string, actor Actor) error { if !x.started.Load() { @@ -775,7 +776,7 @@ func (x *actorSystem) SpawnSingleton(ctx context.Context, name string, actor Act // add the given actor to the tree and supervise it _ = x.actors.AddNode(x.singletonManager, pid) x.actors.AddWatcher(pid, x.deathWatch) - x.broadcastActor(pid, true) + x.broadcastActor(pid) return nil } @@ -1267,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) } @@ -1407,12 +1413,13 @@ func (x *actorSystem) getPeerStateFromStore(address string) (*internalpb.PeerSta } // broadcastActor broadcast the newly (re)spawned actor into the cluster -func (x *actorSystem) broadcastActor(actor *PID, singleton bool) { +func (x *actorSystem) broadcastActor(actor *PID) { if x.clusterEnabled.Load() { x.wireActorsQueue <- &internalpb.ActorRef{ ActorAddress: actor.Address().Address, ActorType: types.Name(actor.Actor()), - IsSingleton: singleton, + IsSingleton: actor.IsSingleton(), + Relocatable: actor.IsRelocatable(), } } } @@ -1860,6 +1867,10 @@ func (x *actorSystem) configPID(ctx context.Context, name string, actor Actor, o pidOpts = append(pidOpts, asSingleton()) } + if !spawnConfig.relocatable { + pidOpts = append(pidOpts, withRelocationDisabled()) + } + // enable stash if x.stashEnabled { pidOpts = append(pidOpts, withStash()) 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 b3398ec0..20ffa15e 100644 --- a/actor/pid.go +++ b/actor/pid.go @@ -146,6 +146,7 @@ type PID struct { goScheduler *goScheduler startedAt *atomic.Int64 isSingleton atomic.Bool + relocatable atomic.Bool } // newPID creates a new pid @@ -192,6 +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.relocatable.Store(true) for _, opt := range opts { opt(pid) @@ -376,11 +378,29 @@ func (pid *PID) IsSuspended() bool { return pid.suspended.Load() } -// IsSingleton returns true when the actor is a singleton +// IsSingleton returns true when the actor is a singleton. +// +// A singleton actor is instantiated when cluster mode is enabled. +// A singleton actor like any other actor is created only once within the system and in the cluster. +// A singleton actor is created with the default supervisor strategy and directive. +// A singleton actor once created lives throughout the lifetime of the given actor system. +// +// The singleton actor is created on the oldest node in the cluster. +// When the oldest node leaves the cluster unexpectedly, the singleton is restarted on the new oldest node. +// This is useful for managing shared resources or coordinating tasks that should be handled by a single actor. func (pid *PID) IsSingleton() bool { return pid.isSingleton.Load() } +// 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.relocatable.Load() +} + // ActorSystem returns the actor system func (pid *PID) ActorSystem() ActorSystem { pid.fieldsLocker.RLock() @@ -564,6 +584,11 @@ func (pid *PID) SpawnChild(ctx context.Context, name string, actor Actor, opts . pidOptions = append(pidOptions, withSupervisor(spawnConfig.supervisor)) } + // set the relocation flag + if spawnConfig.relocatable { + pidOptions = append(pidOptions, withRelocationDisabled()) + } + // disable passivation for system actor switch { case isReservedName(name): @@ -611,7 +636,7 @@ func (pid *PID) SpawnChild(ctx context.Context, name string, actor Actor, opts . } // set the actor in the given actor system registry - pid.ActorSystem().broadcastActor(cid, false) + pid.ActorSystem().broadcastActor(cid) return cid, nil } @@ -1332,6 +1357,7 @@ func (pid *PID) reset() { pid.supervisor.Reset() pid.mailbox.Dispose() pid.isSingleton.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 df3cf627..c352633e 100644 --- a/actor/pid_option.go +++ b/actor/pid_option.go @@ -117,3 +117,10 @@ func asSingleton() pidOption { pid.isSingleton.Store(true) } } + +// withRelocationDisabled disables the actor relocation +func withRelocationDisabled() pidOption { + return func(pid *PID) { + pid.relocatable.Store(false) + } +} diff --git a/actor/pid_option_test.go b/actor/pid_option_test.go index b36a91f0..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 @@ -104,6 +106,11 @@ func TestPIDOptions(t *testing.T) { isSingleton: atomicTrue, }, }, + { + name: "withRelocationDisabled", + option: withRelocationDisabled(), + expected: &PID{relocatable: atomicFalse}, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { diff --git a/actor/rebalancer.go b/actor/rebalancer.go index d24c9dba..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 @@ -131,14 +132,21 @@ func (r *rebalancer) Rebalance(ctx *ReceiveContext) { return NewSpawnError(err) } - if err := r.remoting.RemoteSpawn(egCtx, - peerState.GetHost(), - int(peerState.GetRemotingPort()), - actor.GetActorName(), - actor.GetActorType(), - false); err != nil { - logger.Error(err) - return NewSpawnError(err) + if actor.GetRelocatable() { + host := peerState.GetHost() + port := int(peerState.GetRemotingPort()) + + 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) + } } } } @@ -223,6 +231,10 @@ func (r *rebalancer) recreateLocally(ctx context.Context, actor *internalpb.Acto return r.pid.ActorSystem().SpawnSingleton(ctx, actor.GetActorName(), iactor) } + if !actor.GetRelocatable() { + return nil + } + _, err = r.pid.ActorSystem().Spawn(ctx, actor.GetActorName(), iactor) return err } diff --git a/actor/rebalancer_test.go b/actor/rebalancer_test.go index db93f3d8..6de21e91 100644 --- a/actor/rebalancer_test.go +++ b/actor/rebalancer_test.go @@ -234,3 +234,75 @@ func TestRebalancingWithSingletonActor(t *testing.T) { assert.NoError(t, sd3.Close()) srv.Shutdown() } + +func TestRebalancingWithRelocationDisabled(t *testing.T) { + // create a context + ctx := context.TODO() + // start the NATS server + srv := startNatsServer(t) + + // create and start system cluster + node1, sd1 := startClusterSystem(t, srv.Addr().String()) + require.NotNil(t, node1) + require.NotNil(t, sd1) + + // create and start system cluster + node2, sd2 := startClusterSystem(t, srv.Addr().String()) + require.NotNil(t, node2) + require.NotNil(t, sd2) + + // create and start system cluster + node3, sd3 := startClusterSystem(t, srv.Addr().String()) + require.NotNil(t, node3) + require.NotNil(t, sd3) + + // let us create 4 actors on each node + for j := 1; j <= 4; j++ { + actorName := fmt.Sprintf("Node1-Actor-%d", j) + pid, err := node1.Spawn(ctx, actorName, newMockActor()) + require.NoError(t, err) + require.NotNil(t, pid) + } + + util.Pause(time.Second) + + for j := 1; j <= 4; j++ { + actorName := fmt.Sprintf("Node2-Actor-%d", j) + pid, err := node2.Spawn(ctx, actorName, newMockActor(), WithRelocationDisabled()) + require.NoError(t, err) + require.NotNil(t, pid) + } + + util.Pause(time.Second) + + for j := 1; j <= 4; j++ { + actorName := fmt.Sprintf("Node3-Actor-%d", j) + pid, err := node3.Spawn(ctx, actorName, newMockActor()) + require.NoError(t, err) + require.NotNil(t, pid) + } + + util.Pause(time.Second) + + // take down node2 + require.NoError(t, node2.Stop(ctx)) + require.NoError(t, sd2.Close()) + + // Wait for cluster rebalancing + util.Pause(time.Minute) + + sender, err := node1.LocalActor("Node1-Actor-1") + require.NoError(t, err) + require.NotNil(t, sender) + + // let us access some of the node2 actors from node 1 and node 3 + actorName := "Node2-Actor-1" + err = sender.SendAsync(ctx, actorName, new(testpb.TestSend)) + require.Error(t, err) + + assert.NoError(t, node1.Stop(ctx)) + assert.NoError(t, node3.Stop(ctx)) + assert.NoError(t, sd1.Close()) + assert.NoError(t, sd3.Close()) + srv.Shutdown() +} 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 77a37ab9..aae5a091 100644 --- a/actor/spawn_option.go +++ b/actor/spawn_option.go @@ -40,11 +40,17 @@ type spawnConfig struct { passivateAfter *time.Duration // specifies if the actor is a singleton asSingleton 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) } @@ -113,6 +119,19 @@ func WithLongLived() SpawnOption { }) } +// WithRelocationDisabled prevents the actor from being relocated to another node when cluster mode is active +// and its host node shuts down unexpectedly. By default, actors are relocatable to support system resilience +// and maintain high availability by automatically redeploying them on healthy nodes. +// +// Use this option when you need strict control over an actor's lifecycle and want to ensure that the actor +// is not redeployed after a node failure, such as for actors with node-specific state or dependencies that +// cannot be easily replicated. +func WithRelocationDisabled() SpawnOption { + return spawnOption(func(config *spawnConfig) { + config.relocatable = false + }) +} + // withSingleton ensures that the actor is a singleton. // This is an internal method to set the singleton flag func withSingleton() SpawnOption { diff --git a/actor/spawn_option_test.go b/actor/spawn_option_test.go index 2fc717f0..ea78ca60 100644 --- a/actor/spawn_option_test.go +++ b/actor/spawn_option_test.go @@ -59,13 +59,18 @@ func TestSpawnOption(t *testing.T) { option.Apply(config) require.Equal(t, &spawnConfig{passivateAfter: &longLived}, config) }) - t.Run("spawn option with singleton", func(t *testing.T) { config := &spawnConfig{} option := withSingleton() option.Apply(config) require.Equal(t, &spawnConfig{asSingleton: true}, config) }) + t.Run("spawn option with relocation disabled", func(t *testing.T) { + config := &spawnConfig{} + option := WithRelocationDisabled() + option.Apply(config) + require.Equal(t, &spawnConfig{relocatable: false}, config) + }) } func TestNewSpawnConfig(t *testing.T) { 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/actor.pb.go b/internal/internalpb/actor.pb.go index fbe09727..d76f0977 100644 --- a/internal/internalpb/actor.pb.go +++ b/internal/internalpb/actor.pb.go @@ -30,7 +30,9 @@ type ActorRef struct { // Specifies the actor type ActorType string `protobuf:"bytes,2,opt,name=actor_type,json=actorType,proto3" json:"actor_type,omitempty"` // Specifies if the actor is a singleton - IsSingleton bool `protobuf:"varint,3,opt,name=is_singleton,json=isSingleton,proto3" json:"is_singleton,omitempty"` + IsSingleton bool `protobuf:"varint,3,opt,name=is_singleton,json=isSingleton,proto3" json:"is_singleton,omitempty"` + // Specifies if the actor is disabled for relocation + Relocatable bool `protobuf:"varint,4,opt,name=relocatable,proto3" json:"relocatable,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -86,6 +88,13 @@ func (x *ActorRef) GetIsSingleton() bool { return false } +func (x *ActorRef) GetRelocatable() bool { + if x != nil { + return x.Relocatable + } + return false +} + // ActorProps defines the properties of an actor // that can be used to spawn an actor remotely. type ActorProps struct { @@ -95,7 +104,9 @@ type ActorProps struct { // Specifies the actor type ActorType string `protobuf:"bytes,2,opt,name=actor_type,json=actorType,proto3" json:"actor_type,omitempty"` // Specifies if the actor is a singleton - IsSingleton bool `protobuf:"varint,3,opt,name=is_singleton,json=isSingleton,proto3" json:"is_singleton,omitempty"` + IsSingleton bool `protobuf:"varint,3,opt,name=is_singleton,json=isSingleton,proto3" json:"is_singleton,omitempty"` + // Specifies if the actor is disabled for relocation + Relocatable bool `protobuf:"varint,4,opt,name=relocatable,proto3" json:"relocatable,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -151,13 +162,20 @@ func (x *ActorProps) GetIsSingleton() bool { return false } +func (x *ActorProps) GetRelocatable() bool { + if x != nil { + return x.Relocatable + } + return false +} + var File_internal_actor_proto protoreflect.FileDescriptor var file_internal_actor_proto_rawDesc = string([]byte{ 0x0a, 0x14, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x1a, 0x11, 0x67, 0x6f, 0x61, 0x6b, 0x74, 0x2f, 0x67, 0x6f, 0x61, 0x6b, 0x74, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x83, 0x01, 0x0a, 0x08, 0x41, 0x63, 0x74, 0x6f, 0x72, 0x52, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa5, 0x01, 0x0a, 0x08, 0x41, 0x63, 0x74, 0x6f, 0x72, 0x52, 0x65, 0x66, 0x12, 0x35, 0x0a, 0x0d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x67, 0x6f, 0x61, 0x6b, 0x74, 0x70, 0x62, 0x2e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x52, 0x0c, 0x61, 0x63, 0x74, @@ -165,25 +183,29 @@ var file_internal_actor_proto_rawDesc = string([]byte{ 0x6f, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 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, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, - 0x69, 0x73, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x74, 0x6f, 0x6e, 0x22, 0x6d, 0x0a, 0x0a, 0x41, - 0x63, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x6f, 0x70, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x74, - 0x6f, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, - 0x63, 0x74, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x6f, - 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 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, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x69, - 0x73, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x74, 0x6f, 0x6e, 0x42, 0xa3, 0x01, 0x0a, 0x0e, 0x63, - 0x6f, 0x6d, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x70, 0x62, 0x42, 0x0a, 0x41, - 0x63, 0x74, 0x6f, 0x72, 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, + 0x69, 0x73, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x74, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x0b, 0x72, + 0x65, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0b, 0x72, 0x65, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x22, 0x8f, 0x01, + 0x0a, 0x0a, 0x41, 0x63, 0x74, 0x6f, 0x72, 0x50, 0x72, 0x6f, 0x70, 0x73, 0x12, 0x1d, 0x0a, 0x0a, + 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x61, + 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 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, 0x03, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0b, 0x69, 0x73, 0x53, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x74, 0x6f, 0x6e, 0x12, 0x20, 0x0a, + 0x0b, 0x72, 0x65, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0b, 0x72, 0x65, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x42, + 0xa3, 0x01, 0x0a, 0x0e, 0x63, 0x6f, 0x6d, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x70, 0x62, 0x42, 0x0a, 0x41, 0x63, 0x74, 0x6f, 0x72, 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/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/mocks/cluster/interface.go b/mocks/cluster/interface.go index 9db111e7..9d13b3c5 100644 --- a/mocks/cluster/interface.go +++ b/mocks/cluster/interface.go @@ -387,6 +387,63 @@ func (_c *Interface_IsRunning_Call) RunAndReturn(run func() bool) *Interface_IsR return _c } +// LookupKind provides a mock function with given fields: ctx, kind +func (_m *Interface) LookupKind(ctx context.Context, kind string) (string, error) { + ret := _m.Called(ctx, kind) + + if len(ret) == 0 { + panic("no return value specified for LookupKind") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { + return rf(ctx, kind) + } + if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { + r0 = rf(ctx, kind) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, kind) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Interface_LookupKind_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LookupKind' +type Interface_LookupKind_Call struct { + *mock.Call +} + +// LookupKind is a helper method to define mock.On call +// - ctx context.Context +// - kind string +func (_e *Interface_Expecter) LookupKind(ctx interface{}, kind interface{}) *Interface_LookupKind_Call { + return &Interface_LookupKind_Call{Call: _e.mock.On("LookupKind", ctx, kind)} +} + +func (_c *Interface_LookupKind_Call) Run(run func(ctx context.Context, kind string)) *Interface_LookupKind_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Interface_LookupKind_Call) Return(_a0 string, _a1 error) *Interface_LookupKind_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *Interface_LookupKind_Call) RunAndReturn(run func(context.Context, string) (string, error)) *Interface_LookupKind_Call { + _c.Call.Return(run) + return _c +} + // Peers provides a mock function with given fields: ctx func (_m *Interface) Peers(ctx context.Context) ([]*internalcluster.Peer, error) { ret := _m.Called(ctx) @@ -539,6 +596,53 @@ func (_c *Interface_RemoveActor_Call) RunAndReturn(run func(context.Context, str return _c } +// RemoveKind provides a mock function with given fields: ctx, kind +func (_m *Interface) RemoveKind(ctx context.Context, kind string) error { + ret := _m.Called(ctx, kind) + + if len(ret) == 0 { + panic("no return value specified for RemoveKind") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, kind) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Interface_RemoveKind_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveKind' +type Interface_RemoveKind_Call struct { + *mock.Call +} + +// RemoveKind is a helper method to define mock.On call +// - ctx context.Context +// - kind string +func (_e *Interface_Expecter) RemoveKind(ctx interface{}, kind interface{}) *Interface_RemoveKind_Call { + return &Interface_RemoveKind_Call{Call: _e.mock.On("RemoveKind", ctx, kind)} +} + +func (_c *Interface_RemoveKind_Call) Run(run func(ctx context.Context, kind string)) *Interface_RemoveKind_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Interface_RemoveKind_Call) Return(_a0 error) *Interface_RemoveKind_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *Interface_RemoveKind_Call) RunAndReturn(run func(context.Context, string) error) *Interface_RemoveKind_Call { + _c.Call.Return(run) + return _c +} + // SchedulerJobKeyExists provides a mock function with given fields: ctx, key func (_m *Interface) SchedulerJobKeyExists(ctx context.Context, key string) (bool, error) { ret := _m.Called(ctx, key) diff --git a/protos/internal/actor.proto b/protos/internal/actor.proto index 7ddf9f03..d3af2b40 100644 --- a/protos/internal/actor.proto +++ b/protos/internal/actor.proto @@ -14,6 +14,8 @@ message ActorRef { string actor_type = 2; // Specifies if the actor is a singleton bool is_singleton = 3; + // Specifies if the actor is disabled for relocation + bool relocatable = 4; } // ActorProps defines the properties of an actor @@ -25,4 +27,6 @@ message ActorProps { string actor_type = 2; // Specifies if the actor is a singleton bool is_singleton = 3; + // Specifies if the actor is disabled for relocation + bool relocatable = 4; } 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 + } +}