Skip to content

Commit cfc87d4

Browse files
committed
refactor(go): seperate transport state and session state.
1 parent 5a5bc9a commit cfc87d4

4 files changed

Lines changed: 120 additions & 71 deletions

File tree

foreign/go/client/tcp/tcp_core.go

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ type IggyTcpClient struct {
5959
clientAddress string
6060
currentServerAddress string
6161
connectedAt time.Time
62-
state iggcon.State
62+
transportState iggcon.TransportState
63+
sessionState iggcon.SessionState
6364
// respHeader is the reused response-status read buffer; guarded by c.mtx.
6465
respHeader [ResponseInitialBytesLength]byte
6566
}
@@ -216,7 +217,8 @@ func NewIggyTcpClient(logger *slog.Logger, options ...Option) *IggyTcpClient {
216217
logger: logger,
217218
clientAddress: "",
218219
conn: nil,
219-
state: iggcon.StateDisconnected,
220+
transportState: iggcon.TransportStateDisconnected,
221+
sessionState: iggcon.SessionStateUnauthenticated,
220222
connectedAt: time.Time{},
221223
leaderRedirectionState: iggcon.LeaderRedirectionState{},
222224
currentServerAddress: opts.config.serverAddress,
@@ -341,14 +343,14 @@ func (c *IggyTcpClient) sendWireAndFetchResponse(ctx context.Context, wirePayloa
341343
c.mtx.Lock()
342344
defer c.mtx.Unlock()
343345

344-
switch c.state {
345-
case iggcon.StateShutdown:
346+
switch c.transportState {
347+
case iggcon.TransportStateShutdown:
346348
c.logger.Debug("Cannot send data. Client is shutdown.")
347349
return nil, ierror.ErrClientShutdown
348-
case iggcon.StateDisconnected:
350+
case iggcon.TransportStateDisconnected:
349351
c.logger.Debug("Cannot send data. Client is not connected.")
350352
return nil, ierror.ErrNotConnected
351-
case iggcon.StateConnecting:
353+
case iggcon.TransportStateConnecting:
352354
c.logger.Debug("Cannot send data. Client is still connecting.")
353355
return nil, ierror.ErrNotConnected
354356
}
@@ -431,12 +433,19 @@ func (c *IggyTcpClient) sendLocked(wirePayload []byte) ([]byte, error) {
431433
return buffer, nil
432434
}
433435

436+
func (c *IggyTcpClient) setSessionState(state iggcon.SessionState) {
437+
c.mtx.Lock()
438+
c.sessionState = state
439+
c.mtx.Unlock()
440+
}
441+
434442
// invalidateConnLocked closes the connection and marks it as disconnected
435443
func (c *IggyTcpClient) invalidateConnLocked() {
436444
if c.conn != nil {
437445
_ = c.conn.Close()
438446
}
439-
c.state = iggcon.StateDisconnected
447+
c.transportState = iggcon.TransportStateDisconnected
448+
c.sessionState = iggcon.SessionStateUnauthenticated
440449
}
441450

442451
func (c *IggyTcpClient) GetConnectionInfo() *iggcon.ConnectionInfo {
@@ -451,24 +460,22 @@ func (c *IggyTcpClient) GetConnectionInfo() *iggcon.ConnectionInfo {
451460
// Connect establishes the TCP connection to the server.
452461
func (c *IggyTcpClient) Connect(ctx context.Context) error {
453462
c.mtx.Lock()
454-
switch c.state {
455-
case iggcon.StateShutdown:
463+
switch c.transportState {
464+
case iggcon.TransportStateShutdown:
456465
c.mtx.Unlock()
457466
c.logger.Debug("Cannot connect. Client is shutdown.")
458467
return ierror.ErrClientShutdown
459-
case iggcon.StateConnected,
460-
iggcon.StateAuthenticating,
461-
iggcon.StateAuthenticated:
468+
case iggcon.TransportStateConnected:
462469
clientAddress := c.clientAddress
463470
c.mtx.Unlock()
464471
c.logger.Debug("Client is already connected.", slog.String("client_address", clientAddress))
465472
return nil
466-
case iggcon.StateConnecting:
473+
case iggcon.TransportStateConnecting:
467474
c.mtx.Unlock()
468475
c.logger.Debug("Client is already connecting.")
469476
return nil
470477
default:
471-
c.state = iggcon.StateConnecting
478+
c.transportState = iggcon.TransportStateConnecting
472479
}
473480
connectedAt := c.connectedAt
474481
c.mtx.Unlock()
@@ -543,7 +550,7 @@ func (c *IggyTcpClient) Connect(ctx context.Context) error {
543550
return nil
544551
}); err != nil {
545552
c.mtx.Lock()
546-
c.state = iggcon.StateDisconnected
553+
c.transportState = iggcon.TransportStateDisconnected
547554
c.mtx.Unlock()
548555
if !c.config.reconnection.enabled {
549556
c.logger.Warn("Automatic reconnection is disabled.")
@@ -554,7 +561,7 @@ func (c *IggyTcpClient) Connect(ctx context.Context) error {
554561

555562
c.mtx.Lock()
556563
c.conn = conn
557-
c.state = iggcon.StateConnected
564+
c.transportState = iggcon.TransportStateConnected
558565
c.connectedAt = time.Now()
559566
c.logger.Info("Iggy client has connected to the Iggy server", slog.String("client_address", c.clientAddress), slog.String("server_address", c.currentServerAddress))
560567
c.mtx.Unlock()
@@ -610,12 +617,13 @@ func (c *IggyTcpClient) disconnect() error {
610617
c.mtx.Lock()
611618
defer c.mtx.Unlock()
612619

613-
if c.state == iggcon.StateDisconnected {
620+
if c.transportState == iggcon.TransportStateDisconnected {
614621
return nil
615622
}
616623

617624
c.logger.Info("Iggy client is disconnecting from server...", slog.String("client_address", c.clientAddress))
618-
c.state = iggcon.StateDisconnected
625+
c.transportState = iggcon.TransportStateDisconnected
626+
c.sessionState = iggcon.SessionStateUnauthenticated
619627

620628
if c.conn != nil {
621629
if err := c.conn.Close(); err != nil {
@@ -632,7 +640,7 @@ func (c *IggyTcpClient) shutdown() error {
632640
c.mtx.Lock()
633641
defer c.mtx.Unlock()
634642

635-
if c.state == iggcon.StateShutdown {
643+
if c.transportState == iggcon.TransportStateShutdown {
636644
return nil
637645
}
638646

@@ -644,7 +652,8 @@ func (c *IggyTcpClient) shutdown() error {
644652
}
645653
}
646654

647-
c.state = iggcon.StateShutdown
655+
c.transportState = iggcon.TransportStateShutdown
656+
c.sessionState = iggcon.SessionStateUnauthenticated
648657
c.logger.Info("Iggy TCP client has been shutdown.", slog.String("client_address", c.clientAddress))
649658
// TODO push shutdown event
650659
return nil

foreign/go/client/tcp/tcp_core_test.go

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ func newTestClient(t *testing.T) (*IggyTcpClient, net.Conn) {
4242
t.Helper()
4343
serverConn, clientConn := net.Pipe()
4444
c := &IggyTcpClient{
45-
conn: clientConn,
46-
state: iggcon.StateConnected,
47-
logger: slog.New(slog.DiscardHandler),
45+
conn: clientConn,
46+
transportState: iggcon.TransportStateConnected,
47+
sessionState: iggcon.SessionStateUnauthenticated,
48+
logger: slog.New(slog.DiscardHandler),
4849
}
4950
t.Cleanup(func() {
5051
err := clientConn.Close()
@@ -130,8 +131,8 @@ func TestSendAndFetchResponse_DeadlineTimeout(t *testing.T) {
130131
t.Errorf("got %v, want context.DeadlineExceeded", err)
131132
}
132133
// After a timeout, the connection should be invalidated.
133-
if c.state != iggcon.StateDisconnected {
134-
t.Errorf("expected state %v, got %v", iggcon.StateDisconnected, c.state)
134+
if c.transportState != iggcon.TransportStateDisconnected {
135+
t.Errorf("expected state %v, got %v", iggcon.TransportStateDisconnected, c.transportState)
135136
}
136137

137138
// TODO: revisit after reconnect implementation
@@ -158,8 +159,8 @@ func TestSendAndFetchResponse_CancelDuringIO(t *testing.T) {
158159
t.Errorf("got %v, want context.Canceled", err)
159160
}
160161
// Connection should be invalidated after the I/O error.
161-
if c.state != iggcon.StateDisconnected {
162-
t.Errorf("expected state %v, got %v", iggcon.StateDisconnected, c.state)
162+
if c.transportState != iggcon.TransportStateDisconnected {
163+
t.Errorf("expected state %v, got %v", iggcon.TransportStateDisconnected, c.transportState)
163164
}
164165
}
165166

@@ -214,8 +215,8 @@ func TestSendAndFetchResponse_ErrorStatus(t *testing.T) {
214215
t.Errorf("got %v, want %v", err, ierror.ErrUnauthenticated)
215216
}
216217
// Connection should remain healthy after an application-level error.
217-
if c.state != iggcon.StateConnected {
218-
t.Errorf("expected state %v, got %v", iggcon.StateConnected, c.state)
218+
if c.transportState != iggcon.TransportStateConnected {
219+
t.Errorf("expected state %v, got %v", iggcon.TransportStateConnected, c.transportState)
219220
}
220221
}
221222

@@ -231,8 +232,8 @@ func TestSendAndFetchResponse_SuccessEmptyBody(t *testing.T) {
231232
if len(result) != 0 {
232233
t.Errorf("expected empty result, got %d bytes", len(result))
233234
}
234-
if c.state != iggcon.StateConnected {
235-
t.Errorf("expected state %v, got %v", iggcon.StateConnected, c.state)
235+
if c.transportState != iggcon.TransportStateConnected {
236+
t.Errorf("expected state %v, got %v", iggcon.TransportStateConnected, c.transportState)
236237
}
237238
}
238239

@@ -249,8 +250,8 @@ func TestSendAndFetchResponse_SuccessWithBody(t *testing.T) {
249250
if string(result) != string(body) {
250251
t.Errorf("got %q, want %q", result, body)
251252
}
252-
if c.state != iggcon.StateConnected {
253-
t.Errorf("expected state %v, got %v", iggcon.StateConnected, c.state)
253+
if c.transportState != iggcon.TransportStateConnected {
254+
t.Errorf("expected state %v, got %v", iggcon.TransportStateConnected, c.transportState)
254255
}
255256
}
256257

@@ -272,3 +273,23 @@ func TestNewIggyTcpClient_StoresProvidedLogger(t *testing.T) {
272273
t.Errorf("expected logger output to contain 'source=tcp', got: %q", output)
273274
}
274275
}
276+
277+
func TestLoginUser_RejectedReloginKeepsExistingSession(t *testing.T) {
278+
c, serverConn := newTestClient(t)
279+
c.sessionState = iggcon.SessionStateAuthenticated
280+
281+
go serverRespond(t, serverConn, uint32(ierror.InvalidCredentialsCode), nil)
282+
283+
_, err := c.LoginUser(context.Background(), "other-user", "wrong-password")
284+
if err == nil {
285+
t.Fatal("expected login error, got nil")
286+
}
287+
if !errors.Is(err, ierror.ErrInvalidCredentials) {
288+
t.Errorf("got %v, want %v", err, ierror.ErrInvalidCredentials)
289+
}
290+
// The server rejects the login before touching the existing session,
291+
// so the client must keep reporting the session it still has.
292+
if c.sessionState != iggcon.SessionStateAuthenticated {
293+
t.Errorf("expected session to stay authenticated after rejected relogin, got %v", c.sessionState)
294+
}
295+
}

foreign/go/client/tcp/tcp_session_management.go

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,41 +30,42 @@ import (
3030
)
3131

3232
func (c *IggyTcpClient) LoginUser(ctx context.Context, username string, password string) (*iggcon.IdentityInfo, error) {
33-
c.logger.Info("Iggy client is signing in...", slog.String("client_address", c.clientAddress))
34-
buffer, err := c.do(ctx, &command.LoginUser{
33+
return c.login(ctx, &command.LoginUser{
3534
Username: username,
3635
Password: password,
3736
})
38-
if err != nil {
39-
return nil, err
40-
}
41-
42-
c.logger.Info("Iggy client has signed in successfully.", slog.String("client_address", c.clientAddress))
43-
identity := binaryserialization.DeserializeLogInResponse(buffer)
44-
shouldRedirect, err := c.HandleLeaderRedirection(ctx)
45-
if err != nil {
46-
return nil, err
47-
}
48-
if shouldRedirect {
49-
if err = c.Connect(ctx); err != nil {
50-
return nil, err
51-
}
52-
return c.LoginUser(ctx, username, password)
53-
}
54-
return identity, nil
5537
}
5638

5739
func (c *IggyTcpClient) LoginWithPersonalAccessToken(ctx context.Context, token string) (*iggcon.IdentityInfo, error) {
58-
c.logger.Info("Iggy client is signing in...", slog.String("client_address", c.clientAddress))
59-
buffer, err := c.do(ctx, &command.LoginWithPersonalAccessToken{
40+
return c.login(ctx, &command.LoginWithPersonalAccessToken{
6041
Token: token,
6142
})
43+
}
44+
45+
func (c *IggyTcpClient) login(ctx context.Context, loginCmd command.Command) (*iggcon.IdentityInfo, error) {
46+
c.logger.Info("Iggy client is signing in...", slog.String("client_address", c.clientAddress))
47+
c.mtx.Lock()
48+
pre := c.sessionState
49+
c.sessionState = iggcon.SessionStateAuthenticating
50+
c.mtx.Unlock()
51+
52+
buffer, err := c.do(ctx, loginCmd)
6253
if err != nil {
54+
// A rejected login leaves the session state untouched, so restore the
55+
// pre-login state only while it is still Authenticating: if the state
56+
// moved, the connection may died mid-attempt and invalidation already
57+
// recorded it unauthenticated, skip the restoration.
58+
c.mtx.Lock()
59+
if c.sessionState == iggcon.SessionStateAuthenticating {
60+
c.sessionState = pre
61+
}
62+
c.mtx.Unlock()
6363
return nil, err
6464
}
6565

6666
c.logger.Info("Iggy client has signed in successfully.", slog.String("client_address", c.clientAddress))
6767
identity := binaryserialization.DeserializeLogInResponse(buffer)
68+
c.setSessionState(iggcon.SessionStateAuthenticated)
6869
shouldRedirect, err := c.HandleLeaderRedirection(ctx)
6970
if err != nil {
7071
return nil, err
@@ -73,14 +74,17 @@ func (c *IggyTcpClient) LoginWithPersonalAccessToken(ctx context.Context, token
7374
if err = c.Connect(ctx); err != nil {
7475
return nil, err
7576
}
76-
return c.LoginWithPersonalAccessToken(ctx, token)
77+
return c.login(ctx, loginCmd)
7778
}
7879
return identity, nil
7980
}
8081

8182
func (c *IggyTcpClient) LogoutUser(ctx context.Context) error {
82-
_, err := c.do(ctx, &command.LogoutUser{})
83-
return err
83+
if _, err := c.do(ctx, &command.LogoutUser{}); err != nil {
84+
return err
85+
}
86+
c.setSessionState(iggcon.SessionStateUnauthenticated)
87+
return nil
8488
}
8589

8690
func (c *IggyTcpClient) HandleLeaderRedirection(ctx context.Context) (bool, error) {

foreign/go/contracts/state.go

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,45 @@
1717

1818
package iggcon
1919

20-
type State uint8
20+
type TransportState uint8
2121

2222
const (
23-
StateShutdown State = iota
24-
StateDisconnected
25-
StateConnecting
26-
StateConnected
27-
StateAuthenticating
28-
StateAuthenticated
23+
TransportStateDisconnected TransportState = iota
24+
TransportStateShutdown
25+
TransportStateConnecting
26+
TransportStateConnected
2927
)
3028

31-
func (s State) String() string {
29+
func (s TransportState) String() string {
3230
switch s {
33-
case StateShutdown:
31+
case TransportStateShutdown:
3432
return "shutdown"
35-
case StateDisconnected:
33+
case TransportStateDisconnected:
3634
return "disconnected"
37-
case StateConnecting:
35+
case TransportStateConnecting:
3836
return "connecting"
39-
case StateConnected:
37+
case TransportStateConnected:
4038
return "connected"
41-
case StateAuthenticating:
39+
default:
40+
return "unknown"
41+
}
42+
}
43+
44+
type SessionState uint8
45+
46+
const (
47+
SessionStateUnauthenticated SessionState = iota
48+
SessionStateAuthenticating
49+
SessionStateAuthenticated
50+
)
51+
52+
func (s SessionState) String() string {
53+
switch s {
54+
case SessionStateUnauthenticated:
55+
return "unauthenticated"
56+
case SessionStateAuthenticating:
4257
return "authenticating"
43-
case StateAuthenticated:
58+
case SessionStateAuthenticated:
4459
return "authenticated"
4560
default:
4661
return "unknown"

0 commit comments

Comments
 (0)