This is an automated email from the ASF dual-hosted git repository.
hubcio pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 6ca219f67 refactor(go): separate transport state and session state
(#3652)
6ca219f67 is described below
commit 6ca219f6795769780a671e833939e54fb09ae15a
Author: Chengxi Luo <[email protected]>
AuthorDate: Fri Jul 17 04:48:17 2026 -0400
refactor(go): separate transport state and session state (#3652)
---
foreign/go/client/tcp/tcp_core.go | 50 ++++++++------
foreign/go/client/tcp/tcp_core_test.go | 88 ++++++++++++++++++++-----
foreign/go/client/tcp/tcp_session_management.go | 42 +++++-------
foreign/go/contracts/state.go | 42 +++++++-----
4 files changed, 146 insertions(+), 76 deletions(-)
diff --git a/foreign/go/client/tcp/tcp_core.go
b/foreign/go/client/tcp/tcp_core.go
index 2988db7db..8952fce1a 100644
--- a/foreign/go/client/tcp/tcp_core.go
+++ b/foreign/go/client/tcp/tcp_core.go
@@ -59,7 +59,8 @@ type IggyTcpClient struct {
clientAddress string
currentServerAddress string
connectedAt time.Time
- state iggcon.State
+ transportState iggcon.TransportState
+ sessionState iggcon.SessionState
// respHeader is the reused response-status read buffer; guarded by
c.mtx.
respHeader [ResponseInitialBytesLength]byte
}
@@ -216,7 +217,8 @@ func NewIggyTcpClient(logger *slog.Logger, options
...Option) *IggyTcpClient {
logger: logger,
clientAddress: "",
conn: nil,
- state: iggcon.StateDisconnected,
+ transportState: iggcon.TransportStateDisconnected,
+ sessionState: iggcon.SessionStateUnauthenticated,
connectedAt: time.Time{},
leaderRedirectionState: iggcon.LeaderRedirectionState{},
currentServerAddress: opts.config.serverAddress,
@@ -341,14 +343,14 @@ func (c *IggyTcpClient) sendWireAndFetchResponse(ctx
context.Context, wirePayloa
c.mtx.Lock()
defer c.mtx.Unlock()
- switch c.state {
- case iggcon.StateShutdown:
+ switch c.transportState {
+ case iggcon.TransportStateShutdown:
c.logger.Debug("Cannot send data. Client is shutdown.")
return nil, ierror.ErrClientShutdown
- case iggcon.StateDisconnected:
+ case iggcon.TransportStateDisconnected:
c.logger.Debug("Cannot send data. Client is not connected.")
return nil, ierror.ErrNotConnected
- case iggcon.StateConnecting:
+ case iggcon.TransportStateConnecting:
c.logger.Debug("Cannot send data. Client is still connecting.")
return nil, ierror.ErrNotConnected
}
@@ -431,10 +433,17 @@ func (c *IggyTcpClient) sendLocked(wirePayload []byte)
([]byte, error) {
return buffer, nil
}
+func (c *IggyTcpClient) setSessionState(state iggcon.SessionState) {
+ c.mtx.Lock()
+ c.sessionState = state
+ c.mtx.Unlock()
+}
+
// invalidateConnLocked closes the connection and marks it as disconnected
func (c *IggyTcpClient) invalidateConnLocked() {
_ = c.closeConnLocked()
- c.state = iggcon.StateDisconnected
+ c.transportState = iggcon.TransportStateDisconnected
+ c.sessionState = iggcon.SessionStateUnauthenticated
}
// closeConnLocked closes and drops the current connection.
@@ -459,24 +468,22 @@ func (c *IggyTcpClient) GetConnectionInfo()
*iggcon.ConnectionInfo {
// Connect establishes the TCP connection to the server.
func (c *IggyTcpClient) Connect(ctx context.Context) error {
c.mtx.Lock()
- switch c.state {
- case iggcon.StateShutdown:
+ switch c.transportState {
+ case iggcon.TransportStateShutdown:
c.mtx.Unlock()
c.logger.Debug("Cannot connect. Client is shutdown.")
return ierror.ErrClientShutdown
- case iggcon.StateConnected,
- iggcon.StateAuthenticating,
- iggcon.StateAuthenticated:
+ case iggcon.TransportStateConnected:
clientAddress := c.clientAddress
c.mtx.Unlock()
c.logger.Debug("Client is already connected.",
slog.String("client_address", clientAddress))
return nil
- case iggcon.StateConnecting:
+ case iggcon.TransportStateConnecting:
c.mtx.Unlock()
c.logger.Debug("Client is already connecting.")
return nil
default:
- c.state = iggcon.StateConnecting
+ c.transportState = iggcon.TransportStateConnecting
}
connectedAt := c.connectedAt
c.mtx.Unlock()
@@ -551,7 +558,7 @@ func (c *IggyTcpClient) Connect(ctx context.Context) error {
return nil
}); err != nil {
c.mtx.Lock()
- c.state = iggcon.StateDisconnected
+ c.transportState = iggcon.TransportStateDisconnected
c.mtx.Unlock()
if !c.config.reconnection.enabled {
c.logger.Warn("Automatic reconnection is disabled.")
@@ -562,7 +569,7 @@ func (c *IggyTcpClient) Connect(ctx context.Context) error {
c.mtx.Lock()
c.conn = conn
- c.state = iggcon.StateConnected
+ c.transportState = iggcon.TransportStateConnected
c.connectedAt = time.Now()
c.logger.Info("Iggy client has connected to the Iggy server",
slog.String("client_address", c.clientAddress), slog.String("server_address",
c.currentServerAddress))
c.mtx.Unlock()
@@ -618,12 +625,14 @@ func (c *IggyTcpClient) disconnect() error {
c.mtx.Lock()
defer c.mtx.Unlock()
- if c.state == iggcon.StateDisconnected || c.state ==
iggcon.StateShutdown {
+ if c.transportState == iggcon.TransportStateDisconnected ||
c.transportState == iggcon.TransportStateShutdown {
return nil
}
c.logger.Info("Iggy client is disconnecting from server...",
slog.String("client_address", c.clientAddress))
- c.state = iggcon.StateDisconnected
+ c.transportState = iggcon.TransportStateDisconnected
+ c.sessionState = iggcon.SessionStateUnauthenticated
+
err := c.closeConnLocked()
c.logger.Info("Iggy client has disconnected from server.",
slog.String("client_address", c.clientAddress))
@@ -635,15 +644,16 @@ func (c *IggyTcpClient) shutdown() error {
c.mtx.Lock()
defer c.mtx.Unlock()
- if c.state == iggcon.StateShutdown {
+ if c.transportState == iggcon.TransportStateShutdown {
return nil
}
c.logger.Info("Shutting down the Iggy TCP client...",
slog.String("client_address", c.clientAddress))
err := c.closeConnLocked()
- c.state = iggcon.StateShutdown
+ c.transportState = iggcon.TransportStateShutdown
+ c.sessionState = iggcon.SessionStateUnauthenticated
c.logger.Info("Iggy TCP client has been shutdown.",
slog.String("client_address", c.clientAddress))
// TODO push shutdown event
return err
diff --git a/foreign/go/client/tcp/tcp_core_test.go
b/foreign/go/client/tcp/tcp_core_test.go
index 4f87aeaff..f996f0298 100644
--- a/foreign/go/client/tcp/tcp_core_test.go
+++ b/foreign/go/client/tcp/tcp_core_test.go
@@ -42,9 +42,10 @@ func newTestClient(t *testing.T) (*IggyTcpClient, net.Conn) {
t.Helper()
serverConn, clientConn := net.Pipe()
c := &IggyTcpClient{
- conn: clientConn,
- state: iggcon.StateConnected,
- logger: slog.New(slog.DiscardHandler),
+ conn: clientConn,
+ transportState: iggcon.TransportStateConnected,
+ sessionState: iggcon.SessionStateUnauthenticated,
+ logger: slog.New(slog.DiscardHandler),
}
t.Cleanup(func() {
err := clientConn.Close()
@@ -130,8 +131,8 @@ func TestSendAndFetchResponse_DeadlineTimeout(t *testing.T)
{
t.Errorf("got %v, want context.DeadlineExceeded", err)
}
// After a timeout, the connection should be invalidated.
- if c.state != iggcon.StateDisconnected {
- t.Errorf("expected state %v, got %v", iggcon.StateDisconnected,
c.state)
+ if c.transportState != iggcon.TransportStateDisconnected {
+ t.Errorf("expected state %v, got %v",
iggcon.TransportStateDisconnected, c.transportState)
}
// TODO: revisit after reconnect implementation
@@ -158,8 +159,8 @@ func TestSendAndFetchResponse_CancelDuringIO(t *testing.T) {
t.Errorf("got %v, want context.Canceled", err)
}
// Connection should be invalidated after the I/O error.
- if c.state != iggcon.StateDisconnected {
- t.Errorf("expected state %v, got %v", iggcon.StateDisconnected,
c.state)
+ if c.transportState != iggcon.TransportStateDisconnected {
+ t.Errorf("expected state %v, got %v",
iggcon.TransportStateDisconnected, c.transportState)
}
}
@@ -214,8 +215,8 @@ func TestSendAndFetchResponse_ErrorStatus(t *testing.T) {
t.Errorf("got %v, want %v", err, ierror.ErrUnauthenticated)
}
// Connection should remain healthy after an application-level error.
- if c.state != iggcon.StateConnected {
- t.Errorf("expected state %v, got %v", iggcon.StateConnected,
c.state)
+ if c.transportState != iggcon.TransportStateConnected {
+ t.Errorf("expected state %v, got %v",
iggcon.TransportStateConnected, c.transportState)
}
}
@@ -231,8 +232,8 @@ func TestSendAndFetchResponse_SuccessEmptyBody(t
*testing.T) {
if len(result) != 0 {
t.Errorf("expected empty result, got %d bytes", len(result))
}
- if c.state != iggcon.StateConnected {
- t.Errorf("expected state %v, got %v", iggcon.StateConnected,
c.state)
+ if c.transportState != iggcon.TransportStateConnected {
+ t.Errorf("expected state %v, got %v",
iggcon.TransportStateConnected, c.transportState)
}
}
@@ -249,8 +250,8 @@ func TestSendAndFetchResponse_SuccessWithBody(t *testing.T)
{
if string(result) != string(body) {
t.Errorf("got %q, want %q", result, body)
}
- if c.state != iggcon.StateConnected {
- t.Errorf("expected state %v, got %v", iggcon.StateConnected,
c.state)
+ if c.transportState != iggcon.TransportStateConnected {
+ t.Errorf("expected state %v, got %v",
iggcon.TransportStateConnected, c.transportState)
}
}
@@ -273,6 +274,59 @@ func TestNewIggyTcpClient_StoresProvidedLogger(t
*testing.T) {
}
}
+func TestLoginUser_LoginAndLogout(t *testing.T) {
+ c, serverConn := newTestClient(t)
+
+ identity := make([]byte, 4)
+ binary.LittleEndian.PutUint32(identity, 42)
+
+ go func() {
+ serverRespond(t, serverConn, 0, identity)
+ // login always probes for a leader afterwards; it must be
answered or the call blocks.
+ serverRespond(t, serverConn,
uint32(ierror.FeatureUnavailableCode), nil)
+ serverRespond(t, serverConn, 0, nil)
+ }()
+
+ ctx := context.Background()
+ info, err := c.LoginUser(ctx, "iggy", "iggy")
+ if err != nil {
+ t.Fatalf("unexpected login error: %v", err)
+ }
+ if info.UserId != 42 {
+ t.Errorf("got user id %d, want 42", info.UserId)
+ }
+ if c.sessionState != iggcon.SessionStateAuthenticated {
+ t.Errorf("expected session %v after login, got %v",
iggcon.SessionStateAuthenticated, c.sessionState)
+ }
+
+ if err := c.LogoutUser(ctx); err != nil {
+ t.Fatalf("unexpected logout error: %v", err)
+ }
+ if c.sessionState != iggcon.SessionStateUnauthenticated {
+ t.Errorf("expected session %v after logout, got %v",
iggcon.SessionStateUnauthenticated, c.sessionState)
+ }
+}
+
+func TestLoginUser_RejectedReloginKeepsExistingSession(t *testing.T) {
+ c, serverConn := newTestClient(t)
+ c.sessionState = iggcon.SessionStateAuthenticated
+
+ go serverRespond(t, serverConn, uint32(ierror.InvalidCredentialsCode),
nil)
+
+ _, err := c.LoginUser(context.Background(), "other-user",
"wrong-password")
+ if err == nil {
+ t.Fatal("expected login error, got nil")
+ }
+ if !errors.Is(err, ierror.ErrInvalidCredentials) {
+ t.Errorf("got %v, want %v", err, ierror.ErrInvalidCredentials)
+ }
+ // The server rejects the login before touching the existing session,
+ // so the client must keep reporting the session it still has.
+ if c.sessionState != iggcon.SessionStateAuthenticated {
+ t.Errorf("expected session to stay authenticated after rejected
relogin, got %v", c.sessionState)
+ }
+}
+
var errCloseFailed = errors.New("close failed")
// failingCloseConn is a connection whose Close always fails, standing in for a
@@ -295,8 +349,8 @@ func TestShutdown_FailedCloseStillCompletesTeardown(t
*testing.T) {
if err := c.shutdown(); !errors.Is(err, errCloseFailed) {
t.Fatalf("got %v, want %v", err, errCloseFailed)
}
- if c.state != iggcon.StateShutdown {
- t.Errorf("expected state %v, got %v", iggcon.StateShutdown,
c.state)
+ if c.transportState != iggcon.TransportStateShutdown {
+ t.Errorf("expected state %v, got %v",
iggcon.TransportStateShutdown, c.transportState)
}
if c.conn != nil {
t.Error("expected the closed connection to be dropped")
@@ -320,8 +374,8 @@ func TestDisconnect_ShutdownClientIsNotResurrected(t
*testing.T) {
t.Fatalf("unexpected disconnect error: %v", err)
}
- if c.state != iggcon.StateShutdown {
- t.Errorf("expected state to stay %v, got %v",
iggcon.StateShutdown, c.state)
+ if c.transportState != iggcon.TransportStateShutdown {
+ t.Errorf("expected state to stay %v, got %v",
iggcon.TransportStateShutdown, c.transportState)
}
_, err := c.sendWireAndFetchResponse(context.Background(), emptyWireReq)
diff --git a/foreign/go/client/tcp/tcp_session_management.go
b/foreign/go/client/tcp/tcp_session_management.go
index 9aa65651a..12654de93 100644
--- a/foreign/go/client/tcp/tcp_session_management.go
+++ b/foreign/go/client/tcp/tcp_session_management.go
@@ -30,41 +30,32 @@ import (
)
func (c *IggyTcpClient) LoginUser(ctx context.Context, username string,
password string) (*iggcon.IdentityInfo, error) {
- c.logger.Info("Iggy client is signing in...",
slog.String("client_address", c.clientAddress))
- buffer, err := c.do(ctx, &command.LoginUser{
+ return c.login(ctx, &command.LoginUser{
Username: username,
Password: password,
})
- if err != nil {
- return nil, err
- }
-
- c.logger.Info("Iggy client has signed in successfully.",
slog.String("client_address", c.clientAddress))
- identity := binaryserialization.DeserializeLogInResponse(buffer)
- shouldRedirect, err := c.HandleLeaderRedirection(ctx)
- if err != nil {
- return nil, err
- }
- if shouldRedirect {
- if err = c.Connect(ctx); err != nil {
- return nil, err
- }
- return c.LoginUser(ctx, username, password)
- }
- return identity, nil
}
func (c *IggyTcpClient) LoginWithPersonalAccessToken(ctx context.Context,
token string) (*iggcon.IdentityInfo, error) {
- c.logger.Info("Iggy client is signing in...",
slog.String("client_address", c.clientAddress))
- buffer, err := c.do(ctx, &command.LoginWithPersonalAccessToken{
+ return c.login(ctx, &command.LoginWithPersonalAccessToken{
Token: token,
})
+}
+
+func (c *IggyTcpClient) login(ctx context.Context, loginCmd command.Command)
(*iggcon.IdentityInfo, error) {
+ c.logger.Info("Iggy client is signing in...",
slog.String("client_address", c.clientAddress))
+
+ // A failed login never writes the session state: a server-side reject
+ // leaves the existing session untouched, and a connection that dies
+ // mid-attempt is already reset to unauthenticated by
invalidateConnLocked.
+ buffer, err := c.do(ctx, loginCmd)
if err != nil {
return nil, err
}
c.logger.Info("Iggy client has signed in successfully.",
slog.String("client_address", c.clientAddress))
identity := binaryserialization.DeserializeLogInResponse(buffer)
+ c.setSessionState(iggcon.SessionStateAuthenticated)
shouldRedirect, err := c.HandleLeaderRedirection(ctx)
if err != nil {
return nil, err
@@ -73,14 +64,17 @@ func (c *IggyTcpClient) LoginWithPersonalAccessToken(ctx
context.Context, token
if err = c.Connect(ctx); err != nil {
return nil, err
}
- return c.LoginWithPersonalAccessToken(ctx, token)
+ return c.login(ctx, loginCmd)
}
return identity, nil
}
func (c *IggyTcpClient) LogoutUser(ctx context.Context) error {
- _, err := c.do(ctx, &command.LogoutUser{})
- return err
+ if _, err := c.do(ctx, &command.LogoutUser{}); err != nil {
+ return err
+ }
+ c.setSessionState(iggcon.SessionStateUnauthenticated)
+ return nil
}
func (c *IggyTcpClient) HandleLeaderRedirection(ctx context.Context) (bool,
error) {
diff --git a/foreign/go/contracts/state.go b/foreign/go/contracts/state.go
index d06f043b2..32966b6f1 100644
--- a/foreign/go/contracts/state.go
+++ b/foreign/go/contracts/state.go
@@ -17,30 +17,42 @@
package iggcon
-type State uint8
+type TransportState uint8
const (
- StateShutdown State = iota
- StateDisconnected
- StateConnecting
- StateConnected
- StateAuthenticating
- StateAuthenticated
+ TransportStateDisconnected TransportState = iota
+ TransportStateShutdown
+ TransportStateConnecting
+ TransportStateConnected
)
-func (s State) String() string {
+func (s TransportState) String() string {
switch s {
- case StateShutdown:
+ case TransportStateShutdown:
return "shutdown"
- case StateDisconnected:
+ case TransportStateDisconnected:
return "disconnected"
- case StateConnecting:
+ case TransportStateConnecting:
return "connecting"
- case StateConnected:
+ case TransportStateConnected:
return "connected"
- case StateAuthenticating:
- return "authenticating"
- case StateAuthenticated:
+ default:
+ return "unknown"
+ }
+}
+
+type SessionState uint8
+
+const (
+ SessionStateUnauthenticated SessionState = iota
+ SessionStateAuthenticated
+)
+
+func (s SessionState) String() string {
+ switch s {
+ case SessionStateUnauthenticated:
+ return "unauthenticated"
+ case SessionStateAuthenticated:
return "authenticated"
default:
return "unknown"