Skip to content

Conversation

cnlangzi
Copy link
Member

@cnlangzi cnlangzi commented Aug 10, 2025

Changed

Fixed

Added

Tests

Tasks to complete before merging PR:

  • Ensure unit tests are passing. If not run make unit-tests to check for any regressions 📋
  • Ensure lint tests are passing. if not run make lint to check for any issues
  • Ensure codecov/patch is passing for changes.

Summary by Sourcery

Implement configurable client timeout and KeepAlive support for SSE Server by adding WithClientTimeout option, health check loop with PingEvent and automatic disconnection of stale clients, updating Join signature, and extending tests accordingly

New Features:

  • Add WithClientTimeout option to configure KeepAlive pings and automatic client timeout in SSE Server

Enhancements:

  • Extend Server.Join to return an isNewClient flag and track lastSeen timestamp
  • Implement a health check loop that sends periodic PingEvent and disconnects clients on timeout
  • Introduce ErrClientTimeout and update Client.Send to refresh lastSeen

Tests:

  • Update tests to assert the new Join signature and add timeout test with stdWriter and streamerMock for KeepAlive behavior

Copy link

sourcery-ai bot commented Aug 10, 2025

Reviewer's Guide

Implements a keep‐alive mechanism in the SSE server that periodically pings clients and automatically disconnects those that exceed a configurable idle timeout, introduces a new Option for setting that timeout, and updates the Join API and tests to support these changes.

Sequence diagram for SSE Server KeepAlive health check and client timeout

sequenceDiagram
    participant Server
    participant Client
    participant PingEvent
    loop Every clientTimeout/2 interval
        Server->>Client: Check lastSeen
        alt lastSeen < now - clientTimeout
            Server->>Client: Disconnect (cancel)
            Server->>Server: Remove client from clients/conns
        else lastSeen < now - clientTimeout/2
            Server->>Client: Send PingEvent
        end
    end
Loading

Sequence diagram for updated Join API with new client detection

sequenceDiagram
    participant Server
    participant Client
    participant RW as http.ResponseWriter
    Server->>RW: NewStreamer(rw)
    Server->>Client: Check if client exists
    alt Client does not exist
        Server->>Client: Create new Client
        Server->>Client: Set isNewClient = true
    else Client exists
        Server->>Client: Update connID
        Server->>Client: Set isNewClient = false
    end
    Server->>Client: Update lastSeen
    Server->>RW: Set headers, Flush
    Server-->>Client: Return (client, connID, isNewClient, error)
Loading

Class diagram for updated SSE Server and Client with KeepAlive

classDiagram
    class Server {
        +map[string]*Client clients
        +map[string]int conns
        +context.Context ctx
        +context.CancelCauseFunc cancel
        +time.Duration clientTimeout
        +New(opts ...Option)
        +startHealthCheck()
        +Join(clientID string, rw http.ResponseWriter) (*Client, int, bool, error)
    }
    class Client {
        +string ID
        +int connID
        +Streamer sm
        +context.Context ctx
        +context.CancelCauseFunc cancel
        +time.Time lastSeen
        +Send(evt Event) error
    }
    class Option {
        <<type>>
        +func(*Server)
    }
    Server "1" -- "*" Client : manages
    Option <|.. WithClientTimeout
    class WithClientTimeout {
        +WithClientTimeout(d time.Duration) Option
    }
Loading

File-Level Changes

Change Details Files
Add configurable client timeout and background health check
  • Introduce Option type and WithClientTimeout
  • Store clientTimeout on Server and apply options in New
  • Launch startHealthCheck goroutine using a ticker to ping and prune idle clients
  • Remove timed‐out clients and cancel their contexts
server.go
option.go
Track and update client's lastSeen timestamp
  • Add lastSeen field to Client
  • Update lastSeen on each Send call
  • Use lastSeen for timeout and ping logic in health check
client.go
server.go
Extend Join API to signal new client registrations
  • Change Join signature to return isNewClient flag
  • Initialize lastSeen and set up per‐client context on registration
  • Flush headers as before
server.go
Enhance test suite for keepalive feature
  • Adjust tests to accept new Join return value
  • Add timeout scenario using a custom stdWriter and streamerMock
  • Add unit tests for ping output and ErrClientTimeout behavior
server_test.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

deepsource-io bot commented Aug 10, 2025

Here's the code health analysis summary for commits 1473ef7..dac8533. View details on DeepSource ↗.

Analysis Summary

AnalyzerStatusSummaryLink
DeepSource Go LogoGo✅ SuccessView Check ↗

💡 If you’re a repository administrator, you can configure the quality gates from the settings.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @cnlangzi - I've reviewed your changes and found some issues that need to be addressed.

Blocking issues:

  • The keepAlive logic may incorrectly identify active clients as dead. (link)
Prompt for AI Agents
Please address the comments from this code review:
## Individual Comments

### Comment 1
<location> `ext/sse/server.go:46` </location>
<code_context>
+	return s
+}
+
+func (s *Server) keepAlive() {
+	if s.keepalive <= 0 {
+		return
</code_context>

<issue_to_address>
The keepAlive logic may incorrectly identify active clients as dead.

The conditional appears inverted; clients should be marked dead only if lastSeen is before timeout. Please update the logic accordingly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @cnlangzi - I've reviewed your changes - here's some feedback:

  • Consider renaming the WithTimeout option to something like WithKeepAlive or WithKeepAliveTimeout so its purpose is clearer and doesn’t clash with other timeout configurations.
  • The keep-alive loop and its tests use real time.Sleep, which can slow down and destabilize test runs; consider injecting a clock or timer interface to make the logic more testable and deterministic.
  • startKeepAlive ignores errors from c.Send on ping (using nolint); it would be safer to handle or log these errors so ping failures don’t go completely unnoticed.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Consider renaming the WithTimeout option to something like WithKeepAlive or WithKeepAliveTimeout so its purpose is clearer and doesn’t clash with other timeout configurations.
- The keep-alive loop and its tests use real time.Sleep, which can slow down and destabilize test runs; consider injecting a clock or timer interface to make the logic more testable and deterministic.
- startKeepAlive ignores errors from c.Send on ping (using nolint); it would be safer to handle or log these errors so ping failures don’t go completely unnoticed.

## Individual Comments

### Comment 1
<location> `ext/sse/server_test.go:353` </location>
<code_context>
 	})
+
+	t.Run("timeout", func(t *testing.T) {
+		srv := New(WithTimeout(3 * time.Second))
+		rw := httptest.NewRecorder()
+
+		_, _, _, err := srv.Join("keepalive", rw)
+		require.NoError(t, err)
+
+		sm := &streamerMock{
</code_context>

<issue_to_address>
Missing test for WithTimeout(0) and negative values.

Add tests for WithTimeout(0) and negative values to confirm the keep-alive routine does not start and clients are not timed out unexpectedly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

require.ErrorIs(t, err, ErrNotStreamer)

c1, id, err = srv.Join("join", rw)
c1, id, ok, err = srv.Join("join", rw)
require.NotNil(t, c1)
require.NoError(t, err)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Missing test for WithTimeout(0) and negative values.

Add tests for WithTimeout(0) and negative values to confirm the keep-alive routine does not start and clients are not timed out unexpectedly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sourcery-ai review

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! I'm generating a new review now.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @cnlangzi, I've posted a new review for you!

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @cnlangzi - I've reviewed your changes - here's some feedback:

  • Replace time.Sleep-based tests with a controllable clock or shorter intervals to avoid fragile or slow CI runs.
  • Ensure that concurrent pings sent by startHealthCheck via c.Send won’t race on the Streamer/ResponseWriter, e.g. by serializing writes or flushes.
  • Rename the WithClientTimeout option (or update docs) so its name matches the description (e.g. WithTimeout) for consistency.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- Replace time.Sleep-based tests with a controllable clock or shorter intervals to avoid fragile or slow CI runs.
- Ensure that concurrent pings sent by startHealthCheck via c.Send won’t race on the Streamer/ResponseWriter, e.g. by serializing writes or flushes.
- Rename the WithClientTimeout option (or update docs) so its name matches the description (e.g. WithTimeout) for consistency.

## Individual Comments

### Comment 1
<location> `ext/sse/server_test.go:374` </location>
<code_context>
+		require.Equal(t, body, ": ping\n\n\n")
+		time.Sleep(2 * time.Second)
+
+		err = context.Cause(c.Context())
+
+		require.ErrorIs(t, err, ErrClientTimeout)
+
+	})
</code_context>

<issue_to_address>
Suggestion: Add a test for client activity resetting the timeout.

Please add a test where the client receives an event just before the timeout to confirm lastSeen is updated and the client remains connected, validating the keepalive logic.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +374 to +376
err = context.Cause(c.Context())

require.ErrorIs(t, err, ErrClientTimeout)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Suggestion: Add a test for client activity resetting the timeout.

Please add a test where the client receives an event just before the timeout to confirm lastSeen is updated and the client remains connected, validating the keepalive logic.

@cnlangzi cnlangzi merged commit 6680222 into main Aug 10, 2025
5 checks passed
@cnlangzi cnlangzi deleted the fix/sse_timeout branch August 10, 2025 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant