Repository: Tochemey/goakt
Pull request #608 (2025-01-25 13:27:10)

refactor: enhance remote configuration


Patch:
diff --git a/README.md b/README.md
index 0f7bb60..2d69b24 100644
--- a/README.md
+++ b/README.md
@@ -331,7 +331,7 @@ when creating the actor system. The tasks are executed in the order they have be
 
 ### Remoting
 
-[Remoting](./actors/remoting.go) allows remote actors to communicate. The underlying technology is gRPC. To enable remoting just use the `WithRemoting` option when
+[Remoting](./actors/remoting.go) allows remote actors to communicate. The underlying technology is gRPC. To enable remoting just use the `WithRemote` option when
 creating the actor system. See actor system [options](./actors/option.go). These are the following remoting features available:
 
 - `RemoteTell`: to send a fire-and-forget message to an actor remotely
diff --git a/actors/actor_system.go b/actors/actor_system.go
index 5e4b6d9..896936a 100644
--- a/actors/actor_system.go
+++ b/actors/actor_system.go
@@ -61,10 +61,10 @@ import (
 	"github.com/tochemey/goakt/v2/internal/eventstream"
 	"github.com/tochemey/goakt/v2/internal/internalpb"
 	"github.com/tochemey/goakt/v2/internal/internalpb/internalpbconnect"
-	"github.com/tochemey/goakt/v2/internal/size"
 	"github.com/tochemey/goakt/v2/internal/tcp"
 	"github.com/tochemey/goakt/v2/internal/types"
 	"github.com/tochemey/goakt/v2/log"
+	"github.com/tochemey/goakt/v2/remote"
 )
 
 // ActorSystem defines the contract of an actor system
@@ -169,7 +169,7 @@ type ActorSystem interface {
 	Host() string
 	// Port returns the actor system node port.
 	// This is the bind port for remote communication
-	Port() int32
+	Port() int
 	// Uptime returns the number of seconds since the actor system started
 	Uptime() int64
 	// Running returns true when the actor system is running
@@ -240,13 +240,10 @@ type actorSystem struct {
 	// This allows to handle remote messaging
 	remotingEnabled atomic.Bool
 	remoting        *Remoting
-	// Specifies the remoting port
-	port int32
-	// Specifies the remoting host
-	host string
 	// Specifies the remoting server
-	server   *nethttp.Server
-	listener net.Listener
+	server       *nethttp.Server
+	listener     net.Listener
+	remoteConfig *remote.Config
 
 	// cluster settings
 	clusterEnabled atomic.Bool
@@ -335,8 +332,7 @@ func NewActorSystem(name string, opts ...Option) (ActorSystem, error) {
 		clusterSyncStopSig:     make(chan types.Unit, 1),
 		peersCache:             newPeerCache(),
 		peersStateLoopInterval: DefaultPeerStateLoopInterval,
-		port:                   0,
-		host:                   "127.0.0.1",
+		remoteConfig:           remote.DefaultConfig(),
 		actors:                 newTree(),
 		startedAt:              atomic.NewInt64(0),
 		rebalancing:            atomic.NewBool(false),
@@ -358,8 +354,7 @@ func NewActorSystem(name string, opts ...Option) (ActorSystem, error) {
 		opt.Apply(system)
 	}
 
-	// set the host and port
-	if err := system.setHostPort(); err != nil {
+	if err := system.remoteConfig.Sanitize(); err != nil {
 		return nil, err
 	}
 
@@ -503,16 +498,16 @@ func (x *actorSystem) Uptime() int64 {
 // This is the bind address for remote communication
 func (x *actorSystem) Host() string {
 	x.locker.Lock()
-	host := x.host
+	host := x.remoteConfig.BindAddr()
 	x.locker.Unlock()
 	return host
 }
 
 // Port returns the actor system node port.
 // This is the bind port for remote communication
-func (x *actorSystem) Port() int32 {
+func (x *actorSystem) Port() int {
 	x.locker.Lock()
-	port := x.port
+	port := x.remoteConfig.BindPort()
 	x.locker.Unlock()
 	return port
 }
@@ -942,7 +937,7 @@ func (x *actorSystem) RemoteLookup(ctx context.Context, request *connect.Request
 		return nil, connect.NewError(connect.CodeFailedPrecondition, ErrRemotingDisabled)
 	}
 
-	remoteAddr := fmt.Sprintf("%s:%d", x.host, x.port)
+	remoteAddr := fmt.Sprintf("%s:%d", x.remoteConfig.BindAddr(), x.remoteConfig.BindPort())
 	if remoteAddr != net.JoinHostPort(msg.GetHost(), strconv.Itoa(int(msg.GetPort()))) {
 		return nil, connect.NewError(connect.CodeInvalidArgument, ErrInvalidHost)
 	}
@@ -1004,7 +999,7 @@ func (x *actorSystem) RemoteAsk(ctx context.Context, stream *connect.BidiStream[
 		receiver := message.GetReceiver()
 		name := receiver.GetName()
 
-		remoteAddr := fmt.Sprintf("%s:%d", x.host, x.port)
+		remoteAddr := fmt.Sprintf("%s:%d", x.remoteConfig.BindAddr(), x.remoteConfig.BindPort())
 		if remoteAddr != net.JoinHostPort(receiver.GetHost(), strconv.Itoa(int(receiver.GetPort()))) {
 			return connect.NewError(connect.CodeInvalidArgument, ErrInvalidHost)
 		}
@@ -1107,7 +1102,7 @@ func (x *actorSystem) RemoteReSpawn(ctx context.Context, request *connect.Reques
 		return nil, connect.NewError(connect.CodeFailedPrecondition, ErrRemotingDisabled)
 	}
 
-	remoteAddr := fmt.Sprintf("%s:%d", x.host, x.port)
+	remoteAddr := fmt.Sprintf("%s:%d", x.remoteConfig.BindAddr(), x.remoteConfig.BindPort())
 	if remoteAddr != net.JoinHostPort(msg.GetHost(), strconv.Itoa(int(msg.GetPort()))) {
 		return nil, connect.NewError(connect.CodeInvalidArgument, ErrInvalidHost)
 	}
@@ -1146,7 +1141,7 @@ func (x *actorSystem) RemoteStop(ctx context.Context, request *connect.Request[i
 		return nil, connect.NewError(connect.CodeFailedPrecondition, ErrRemotingDisabled)
 	}
 
-	remoteAddr := fmt.Sprintf("%s:%d", x.host, x.port)
+	remoteAddr := fmt.Sprintf("%s:%d", x.remoteConfig.BindAddr(), x.remoteConfig.BindPort())
 	if remoteAddr != net.JoinHostPort(msg.GetHost(), strconv.Itoa(int(msg.GetPort()))) {
 		return nil, connect.NewError(connect.CodeInvalidArgument, ErrInvalidHost)
 	}
@@ -1175,7 +1170,7 @@ func (x *actorSystem) RemoteSpawn(ctx context.Context, request *connect.Request[
 		return nil, connect.NewError(connect.CodeFailedPrecondition, ErrRemotingDisabled)
 	}
 
-	remoteAddr := fmt.Sprintf("%s:%d", x.host, x.port)
+	remoteAddr := fmt.Sprintf("%s:%d", x.remoteConfig.BindAddr(), x.remoteConfig.BindPort())
 	if remoteAddr != net.JoinHostPort(msg.GetHost(), strconv.Itoa(int(msg.GetPort()))) {
 		return nil, connect.NewError(connect.CodeInvalidArgument, ErrInvalidHost)
 	}
@@ -1211,7 +1206,7 @@ func (x *actorSystem) GetNodeMetric(_ context.Context, request *connect.Request[
 
 	req := request.Msg
 
-	remoteAddr := fmt.Sprintf("%s:%d", x.host, x.port)
+	remoteAddr := fmt.Sprintf("%s:%d", x.remoteConfig.BindAddr(), x.remoteConfig.BindPort())
 	if remoteAddr != req.GetNodeAddress() {
 		return nil, connect.NewError(connect.CodeInvalidArgument, ErrInvalidHost)
 	}
@@ -1232,7 +1227,7 @@ func (x *actorSystem) GetKinds(_ context.Context, request *connect.Request[inter
 	}
 
 	req := request.Msg
-	remoteAddr := fmt.Sprintf("%s:%d", x.host, x.port)
+	remoteAddr := fmt.Sprintf("%s:%d", x.remoteConfig.BindAddr(), x.remoteConfig.BindPort())
 
 	// routine check
 	if remoteAddr != req.GetNodeAddress() {
@@ -1347,10 +1342,10 @@ func (x *actorSystem) enableClustering(ctx context.Context) error {
 
 	x.clusterNode = &discovery.Node{
 		Name:          x.Name(),
-		Host:          x.host,
+		Host:          x.remoteConfig.BindAddr(),
 		DiscoveryPort: x.clusterConfig.DiscoveryPort(),
 		PeersPort:     x.clusterConfig.PeersPort(),
-		RemotingPort:  int(x.port),
+		RemotingPort:  x.remoteConfig.BindPort(),
 	}
 
 	clusterEngine, err := cluster.NewEngine(
@@ -1802,7 +1797,7 @@ func (x *actorSystem) reservedName(nameType nameType) string {
 
 // actorAddress returns the actor path provided the actor name
 func (x *actorSystem) actorAddress(name string) *address.Address {
-	return address.New(name, x.name, x.host, int(x.port))
+	return address.New(name, x.name, x.remoteConfig.BindAddr(), x.remoteConfig.BindPort())
 }
 
 // startHTTPServer starts the appropriate http server
@@ -1817,7 +1812,7 @@ func (x *actorSystem) shutdownHTTPServer(ctx context.Context) error {
 
 // configureServer configure the various http server and listeners based upon the various settings
 func (x *actorSystem) configureServer(ctx context.Context, mux *nethttp.ServeMux) error {
-	hostPort := net.JoinHostPort(x.host, strconv.Itoa(int(x.port)))
+	hostPort := net.JoinHostPort(x.remoteConfig.BindAddr(), strconv.Itoa(x.remoteConfig.BindPort()))
 	httpServer := getServer(ctx, hostPort)
 	listener, err := tcp.NewKeepAliveListener(httpServer.Addr)
 	if err != nil {
@@ -1826,9 +1821,11 @@ func (x *actorSystem) configureServer(ctx context.Context, mux *nethttp.ServeMux
 
 	// Configure HTTP/2 with performance tuning
 	http2Server := &http2.Server{
-		MaxConcurrentStreams: 1000,               // Allow up to 1000 concurrent streams
-		MaxReadFrameSize:     16 * size.MB,       // 16 MB max frame size
-		IdleTimeout:          1200 * time.Second, // Timeout for idle connections
+		MaxConcurrentStreams: 1000, // Allow up to 1000 concurrent streams
+		MaxReadFrameSize:     x.remoteConfig.MaxFrameSize(),
+		IdleTimeout:          x.remoteConfig.IdleTimeout(),
+		WriteByteTimeout:     x.remoteConfig.WriteTimeout(),
+		ReadIdleTimeout:      x.remoteConfig.ReadIdleTimeout(),
 	}
 
 	// set the http TLS server
@@ -1847,18 +1844,6 @@ func (x *actorSystem) configureServer(ctx context.Context, mux *nethttp.ServeMux
 	return nil
 }
 
-// setHostPort sets the host and port
-func (x *actorSystem) setHostPort() error {
-	var err error
-	// combine host and port into an hostPort string
-	hostPort := net.JoinHostPort(x.host, strconv.Itoa(int(x.port)))
-	x.host, err = tcp.GetBindIP(hostPort)
-	if err != nil {
-		return err
-	}
-	return nil
-}
-
 // spawnRootGuardian creates the rootGuardian guardian
 func (x *actorSystem) spawnRootGuardian(ctx context.Context) error {
 	var err error
diff --git a/actors/option.go b/actors/option.go
index ce3b31e..6d21740 100644
--- a/actors/option.go
+++ b/actors/option.go
@@ -29,7 +29,9 @@ import (
 	"time"
 
 	"github.com/tochemey/goakt/v2/hash"
+	"github.com/tochemey/goakt/v2/internal/size"
 	"github.com/tochemey/goakt/v2/log"
+	"github.com/tochemey/goakt/v2/remote"
 )
 
 // ShutdownHook defines the shutdown hook to be executed alongside the
@@ -90,16 +92,24 @@ func WithPassivationDisabled() Option {
 }
 
 // WithRemoting enables remoting on the actor system
+// Deprecated: use WithRemote which provides better remoting configuration and optimization
 func WithRemoting(host string, port int32) Option {
 	return OptionFunc(
 		func(a *actorSystem) {
 			a.remotingEnabled.Store(true)
-			a.port = port
-			a.host = host
+			a.remoteConfig = remote.NewConfig(host, int(port), 16*size.MB)
 		},
 	)
 }
 
+// WithRemote enables remoting on the actor system
+func WithRemote(config *remote.Config) Option {
+	return OptionFunc(func(a *actorSystem) {
+		a.remotingEnabled.Store(true)
+		a.remoteConfig = config
+	})
+}
+
 // WithCluster enables the cluster mode
 func WithCluster(config *ClusterConfig) Option {
 	return OptionFunc(
diff --git a/remote/config.go b/remote/config.go
new file mode 100644
index 0000000..6d15dbd
--- /dev/null
+++ b/remote/config.go
@@ -0,0 +1,141 @@
+/*
+ * 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 (
+	"net"
+	"strconv"
+	"time"
+
+	"github.com/tochemey/goakt/v2/internal/size"
+	"github.com/tochemey/goakt/v2/internal/tcp"
+	"github.com/tochemey/goakt/v2/internal/validation"
+)
+
+// Config defines the remote config
+type Config struct {
+	maxFrameSize    uint32
+	writeTimeout    time.Duration
+	readIdleTimeout time.Duration
+	idleTimeout     time.Duration
+	bindAddr        string
+	bindPort        int
+}
+
+var _ validation.Validator = (*Config)(nil)
+
+// NewConfig creates an instance of remote config
+func NewConfig(host string, port int, maxFrameSize uint32, opts ...Option) *Config {
+	cfg := &Config{
+		maxFrameSize:    maxFrameSize,
+		writeTimeout:    10 * time.Second,
+		readIdleTimeout: 10 * time.Second,
+		idleTimeout:     1200 * time.Second,
+		bindAddr:        host,
+		bindPort:        port,
+	}
+
+	// apply the options
+	for _, opt := range opts {
+		opt.Apply(cfg)
+	}
+
+	return cfg
+}
+
+// DefaultConfig returns the default remote config
+func DefaultConfig() *Config {
+	return &Config{
+		maxFrameSize:    16 * size.MB,
+		writeTimeout:    10 * time.Second,
+		readIdleTimeout: 10 * time.Second,
+		idleTimeout:     1200 * time.Second,
+		bindAddr:        "127.0.0.1",
+		bindPort:        0,
+	}
+}
+
+// IdleTimeout specifies how long until idle clients should be
+// closed with a GOAWAY frame. PING frames are not considered
+// activity for the purposes of IdleTimeout.
+// If zero or negative, there is no timeout.
+func (x *Config) IdleTimeout() time.Duration {
+	return x.idleTimeout
+}
+
+// MaxFrameSize specifies the largest frame
+// this server is willing to read. A valid value is between
+// 16k and 16M, inclusive. If zero or otherwise invalid, an error will be thrown.
+func (x *Config) MaxFrameSize() uint32 {
+	return x.maxFrameSize
+}
+
+// WriteTimeout is the timeout after which a connection will be
+// closed if no data can be written to it. The timeout begins when data is
+// available to write, and is extended whenever any bytes are written.
+// If zero or negative, there is no timeout.
+func (x *Config) WriteTimeout() time.Duration {
+	return x.writeTimeout
+}
+
+// ReadIdleTimeout is the timeout after which a health check using a ping
+// frame will be carried out if no frame is received on the connection.
+// If zero, no health check is performed.
+func (x *Config) ReadIdleTimeout() time.Duration {
+	return x.readIdleTimeout
+}
+
+// BindAddr returns the bind addr
+func (x *Config) BindAddr() string {
+	return x.bindAddr
+}
+
+// BindPort returns the bind port
+func (x *Config) BindPort() int {
+	return x.bindPort
+}
+
+// Sanitize the configuration
+func (x *Config) Sanitize() error {
+	var err error
+	// combine host and port into an hostPort string
+	hostPort := net.JoinHostPort(x.bindAddr, strconv.Itoa(int(x.bindPort)))
+	x.bindAddr, err = tcp.GetBindIP(hostPort)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (x *Config) Validate() error {
+	return validation.
+		New(validation.FailFast()).
+		AddValidator(validation.NewEmptyStringValidator("bindAddr", x.bindAddr)).
+		AddAssertion(x.maxFrameSize >= 16*size.KB && x.maxFrameSize <= 16*size.MB, "maxFrameSize must be between 16KB and 16MB").
+		AddAssertion(x.bindPort >= 0 && x.bindPort <= 65535, "invalid bindPort").
+		AddAssertion(x.readIdleTimeout >= 0, "invalid server read idle timeout").
+		AddAssertion(x.writeTimeout >= 0, "invalid server write timeout").
+		Validate()
+}
diff --git a/remote/option.go b/remote/option.go
new file mode 100644
index 0000000..4761e9d
--- /dev/null
+++ b/remote/option.go
@@ -0,0 +1,69 @@
+/*
+ * 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 "time"
+
+// Option is the interface that applies a configuration option.
+type Option interface {
+	// Apply sets the Option value of a config.
+	Apply(*Config)
+}
+
+// enforce compilation error
+var _ Option = OptionFunc(nil)
+
+// OptionFunc implements the Option interface.
+type OptionFunc func(config *Config)
+
+func (f OptionFunc) Apply(c *Config) {
+	f(c)
+}
+
+// WithWriteTimeout sets the write timeout
+func WithWriteTimeout(timeout time.Duration) Option {
+	return OptionFunc(func(config *Config) {
+		config.writeTimeout = timeout
+	})
+}
+
+// WithReadIdleTimeout sets the read timeout
+// ReadIdleTimeout is the timeout after which a health check using a ping
+// frame will be carried out if no frame is received on the connection.
+// If zero, no health check is performed.
+func WithReadIdleTimeout(timeout time.Duration) Option {
+	return OptionFunc(func(config *Config) {
+		config.readIdleTimeout = timeout
+	})
+}
+
+// WithMaxFrameSize specifies the largest frame
+// this server is willing to read. A valid value is between
+// 16k and 16M, inclusive. If zero or otherwise invalid, an error will be thrown.
+func WithMaxFrameSize(size uint32) Option {
+	return OptionFunc(func(config *Config) {
+		config.maxFrameSize = size
+	})
+}


Test patch:
diff --git a/actors/option_test.go b/actors/option_test.go
index 005adbf..2e6a718 100644
--- a/actors/option_test.go
+++ b/actors/option_test.go
@@ -35,6 +35,7 @@ import (
 
 	"github.com/tochemey/goakt/v2/hash"
 	"github.com/tochemey/goakt/v2/log"
+	"github.com/tochemey/goakt/v2/remote"
 )
 
 func TestOption(t *testing.T) {
@@ -48,6 +49,9 @@ func TestOption(t *testing.T) {
 		ClientConfig: tlsConfig,
 		ServerConfig: tlsConfig,
 	}
+
+	remoteConfig := remote.DefaultConfig()
+
 	testCases := []struct {
 		name     string
 		option   Option
@@ -73,11 +77,6 @@ func TestOption(t *testing.T) {
 			option:   WithPassivationDisabled(),
 			expected: actorSystem{expireActorAfter: -1},
 		},
-		{
-			name:     "WithRemoting",
-			option:   WithRemoting("localhost", 3100),
-			expected: actorSystem{remotingEnabled: atomicTrue, port: 3100, host: "localhost"},
-		},
 		{
 			name:     "WithShutdownTimeout",
 			option:   WithShutdownTimeout(2 * time.Second),
@@ -118,6 +117,11 @@ func TestOption(t *testing.T) {
 			option:   WithTLS(tlsInfo),
 			expected: actorSystem{tlsServerConfig: tlsConfig, tlsClientConfig: tlsConfig},
 		},
+		{
+			name:     "WithRemote",
+			option:   WithRemote(remoteConfig),
+			expected: actorSystem{remotingEnabled: atomicTrue, remoteConfig: remoteConfig},
+		},
 	}
 
 	for _, tc := range testCases {
diff --git a/remote/config_test.go b/remote/config_test.go
new file mode 100644
index 0000000..dee9acd
--- /dev/null
+++ b/remote/config_test.go
@@ -0,0 +1,60 @@
+/*
+ * 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 (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"github.com/tochemey/goakt/v2/internal/size"
+)
+
+func TestConfig(t *testing.T) {
+	t.Run("With default config", func(t *testing.T) {
+		config := DefaultConfig()
+		require.NoError(t, config.Validate())
+		require.NoError(t, config.Sanitize())
+		assert.EqualValues(t, 16*size.MB, config.MaxFrameSize())
+		assert.Exactly(t, 10*time.Second, config.WriteTimeout())
+		assert.Exactly(t, 10*time.Second, config.ReadIdleTimeout())
+		assert.Exactly(t, 1200*time.Second, config.IdleTimeout())
+		assert.Exactly(t, "127.0.0.1", config.BindAddr())
+		assert.Exactly(t, 0, config.BindPort())
+	})
+	t.Run("With invalid framesize", func(t *testing.T) {
+		config := NewConfig("127.0.0.1", 8080, 20*size.MB)
+		err := config.Validate()
+		require.Error(t, err)
+		assert.EqualError(t, err, "maxFrameSize must be between 16KB and 16MB")
+	})
+	t.Run("With invalid bindAddr", func(t *testing.T) {
+		config := NewConfig("invalid@adrr", 8080, 20*size.MB)
+		err := config.Sanitize()
+		require.Error(t, err)
+	})
+}
diff --git a/remote/option_test.go b/remote/option_test.go
new file mode 100644
index 0000000..9dfeac7
--- /dev/null
+++ b/remote/option_test.go
@@ -0,0 +1,64 @@
+/*
+ * 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 (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestOption(t *testing.T) {
+	testCases := []struct {
+		name     string
+		option   Option
+		expected Config
+	}{
+		{
+			name:     "WithWriteTimeout",
+			option:   WithWriteTimeout(10 * time.Second),
+			expected: Config{writeTimeout: 10 * time.Second},
+		},
+		{
+			name:     "WithReadIdleTimeout",
+			option:   WithReadIdleTimeout(10 * time.Second),
+			expected: Config{readIdleTimeout: 10 * time.Second},
+		},
+		{
+			name:     "WithMaxFrameSize",
+			option:   WithMaxFrameSize(1024),
+			expected: Config{maxFrameSize: 1024},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			var config Config
+			tc.option.Apply(&config)
+			assert.Equal(t, tc.expected, config)
+		})
+	}
+}