Skip to content

feat: add ping for sse server #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 11, 2025
Merged

feat: add ping for sse server #80

merged 11 commits into from
Apr 11, 2025

Conversation

lcgash
Copy link
Contributor

@lcgash lcgash commented Mar 27, 2025

Same with #78.

Summary by CodeRabbit

  • New Features
    • Introduced a keep-alive mechanism for server-sent events that periodically sends ping messages to maintain active connections.
    • Added configurable options to enable the keep-alive functionality and adjust the interval between pings, enhancing connection stability.

Copy link
Contributor

coderabbitai bot commented Mar 27, 2025

Walkthrough

The update introduces two new fields, keepAlive and keepAliveInterval, to the SSEServer struct in the server/sse.go file. Corresponding methods, WithKeepAlive and WithKeepAliveInterval, are added to allow configuration of these fields. Default values for keepAlive (false) and keepAliveInterval (10 seconds) are set in the NewSSEServer function. Additionally, a new logic block in the handleSSE method manages keep-alive functionality by sending periodic "ping" events if keepAlive is enabled.

Changes

File(s) Change Summary
server/sse.go - Added keepAlive and keepAliveInterval fields to SSEServer
- Introduced WithKeepAlive and WithKeepAliveInterval methods for configuration
- Updated NewSSEServer to set default values for keepAlive and keepAliveInterval
- Modified handleSSE to implement keep-alive message sending logic

Tip

⚡💬 Agentic Chat (Pro Plan, General Availability)
  • We're introducing multi-step agentic chat in review comments and issue comments, within and outside of PR's. This feature enhances review and issue discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments and add commits to existing pull requests.
✨ Finishing Touches
  • 📝 Generate Docstrings

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai plan to trigger planning for file edits and PR creation.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
server/sse.go (2)

127-138: Good implementation of option functions

The implementation of WithKeepAliveInterval and WithKeepAlive follows the existing pattern in the codebase. I like how WithKeepAliveInterval automatically enables keep-alive when setting the interval.

Consider adding documentation comments for these exported functions to explain their purpose and behavior.

+// WithKeepAliveInterval sets the interval for keep-alive pings and enables the keep-alive feature
 func WithKeepAliveInterval(keepAliveInterval time.Duration) SSEOption {
     return func(s *SSEServer) {
         s.keepAlive = true
         s.keepAliveInterval = keepAliveInterval
     }
 }

+// WithKeepAlive enables or disables the keep-alive ping mechanism
 func WithKeepAlive(keepAlive bool) SSEOption {
     return func(s *SSEServer) {
         s.keepAlive = keepAlive
     }
 }

266-282: Well-implemented keep-alive mechanism

The keep-alive implementation correctly:

  1. Only runs when the feature is enabled
  2. Uses a ticker with the configured interval
  3. Properly cleans up resources with defer ticker.Stop()
  4. Handles termination conditions through multiple select cases
  5. Formats ping events according to SSE standards

Consider adding validation for very short keep-alive intervals that could impact performance, and consider handling the case where the event queue might be full (similar to the check in SendEventToSession).

 // Start keep alive : ping
 if s.keepAlive {
+    // Ensure minimum interval to prevent performance issues
+    if s.keepAliveInterval < 100 * time.Millisecond {
+        s.keepAliveInterval = 100 * time.Millisecond
+    }
     go func() {
         ticker := time.NewTicker(s.keepAliveInterval)
         defer ticker.Stop()
         for {
             select {
             case <-ticker.C:
-                session.eventQueue <- fmt.Sprintf("event: ping\ndata: %s\n\n", time.Now().Format(time.RFC3339))
+                select {
+                    case session.eventQueue <- fmt.Sprintf("event: ping\ndata: %s\n\n", time.Now().Format(time.RFC3339)):
+                        // Event queued successfully
+                    case <-session.done:
+                        return
+                    default:
+                        // Queue is full, could log this
+                }
             case <-session.done:
                 return
             case <-r.Context().Done():
                 return
             }
         }
     }()
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d3dc35c and 25a4798.

📒 Files selected for processing (1)
  • server/sse.go (5 hunks)
🔇 Additional comments (4)
server/sse.go (4)

13-13: Great addition of the time package

The addition of the time package is necessary to support the new keep-alive functionality with timer operations and duration types.


65-66: Well-structured fields for the keep-alive mechanism

These fields clearly define the configuration parameters for the SSE keep-alive feature. The boolean toggle and configurable interval provide good flexibility.


154-155: Sensible default values

Setting keep-alive to disabled by default with a 10-second interval is a good approach. It allows users to opt into the feature while having reasonable defaults when they do.


266-282: Consider adding tests for the keep-alive functionality

This PR introduces important functionality for maintaining connection stability, but I don't see tests for this new feature. Consider adding tests to verify:

  1. Pings are sent at the expected intervals when enabled
  2. No pings are sent when the feature is disabled
  3. The option functions correctly configure the server

Do you need assistance with creating tests for this feature?

@kingster
Copy link

kingster commented Apr 1, 2025

@ezynda3 Could you please verify this pr/ merge this?

For running sse behind Load Balancers, this is critical.

@ezynda3
Copy link
Contributor

ezynda3 commented Apr 2, 2025

@kingster I am on holiday until the 10th, so I am not sure if I will have a spare moment to review this until then. If I do, I will definitely have a look. 🙏

@lcgash
Copy link
Contributor Author

lcgash commented Apr 8, 2025

@ezynda3 Could you please verify this pr/ merge this?

For running sse behind Load Balancers, this is critical.

i have tested it with our prod env, If you need it, you can try it directly with my repo

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
server/sse.go (2)

137-142: Consider making behavior more explicit

WithKeepAliveInterval implicitly enables keep-alive by setting s.keepAlive = true. This implicit behavior might not be immediately obvious to users.

Consider either:

  1. Renaming to WithEnabledKeepAliveInterval to make it clear that this also enables the feature, or
  2. Adding a comment that documents this behavior
 func WithKeepAliveInterval(keepAliveInterval time.Duration) SSEOption {
 	return func(s *SSEServer) {
+		// Setting an interval implicitly enables keep-alive
 		s.keepAlive = true
 		s.keepAliveInterval = keepAliveInterval
 	}
 }

286-286: Consider making the ping format customizable

The current implementation uses a fixed format for ping messages (:ping - <timestamp>). In some scenarios, it might be beneficial to allow customization of this format.

While not necessary for the current requirements, a future enhancement could allow for customizable ping formats:

// Add to SSEServer struct
+	pingFormat string

// Add new option
+func WithPingFormat(format string) SSEOption {
+	return func(s *SSEServer) {
+		s.pingFormat = format
+	}
+}

// Initialize in NewSSEServer
+		pingFormat:        ":ping - %s\n\n",

// Use in ping message
-					session.eventQueue <- fmt.Sprintf(":ping - %s\n\n", time.Now().Format(time.RFC3339))
+					session.eventQueue <- fmt.Sprintf(s.pingFormat, time.Now().Format(time.RFC3339))
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 993d9f0 and 9faca53.

📒 Files selected for processing (1)
  • server/sse.go (5 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
server/sse.go (2)
server/server.go (1)
  • MCPServer (138-152)
examples/custom_context/main.go (1)
  • MCPServer (100-102)
🔇 Additional comments (6)
server/sse.go (6)

13-13: LGTM: Import of time package

The time package is correctly imported to support the new keep-alive functionality.


66-67: LGTM: New fields for keep-alive functionality

The addition of keepAlive and keepAliveInterval fields to the SSEServer struct properly supports the implementation of ping messages for SSE connections.


144-148: LGTM: Explicit keep-alive option

This option function provides a clear way to enable or disable the keep-alive functionality.


165-166: LGTM: Default values for keep-alive configuration

The default values are sensible: keep-alive is disabled by default, and the interval is set to 10 seconds (a reasonable default for keeping connections alive behind load balancers).


278-294: LGTM: Keep-alive ping implementation

This implementation follows good practices:

  1. It only runs when keep-alive is enabled
  2. Uses a ticker for precise timing
  3. Properly cleans up resources with defer ticker.Stop()
  4. Handles termination scenarios (session done, request context done)
  5. Uses the standard SSE comment format (:ping) for the keep-alive message

The implementation will effectively keep connections open behind load balancers that might otherwise close idle connections.


296-296: LGTM: Message endpoint construction

This change correctly builds the message endpoint URL with the session ID.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
server/sse.go (1)

286-291: Good implementation of non-blocking ping with error logging.

The implementation now properly handles the case where the event queue is full by using a non-blocking channel send with the select statement and logging warnings for dropped pings.

🧹 Nitpick comments (3)
server/sse.go (3)

137-142: Consider separating configuration concerns.

The WithKeepAliveInterval function automatically enables keep-alive by setting s.keepAlive = true. This could cause unexpected behavior if a user only wants to configure the interval without enabling the feature yet.

Consider separating these concerns:

func WithKeepAliveInterval(keepAliveInterval time.Duration) SSEOption {
	return func(s *SSEServer) {
-		s.keepAlive = true
		s.keepAliveInterval = keepAliveInterval
	}
}

278-299: Consider adding validation for keepAliveInterval.

The implementation doesn't validate the keepAliveInterval value. Very short intervals could cause excessive network traffic or overload the client, while very long intervals might not be effective at keeping connections alive with some load balancers.

Consider adding validation in the WithKeepAliveInterval function:

func WithKeepAliveInterval(keepAliveInterval time.Duration) SSEOption {
	return func(s *SSEServer) {
+		// Ensure reasonable limits for the interval
+		if keepAliveInterval < time.Second {
+			keepAliveInterval = time.Second // Minimum 1 second
+		} else if keepAliveInterval > 2 * time.Minute {
+			keepAliveInterval = 2 * time.Minute // Maximum 2 minutes
+		}
		s.keepAlive = true
		s.keepAliveInterval = keepAliveInterval
	}
}

301-302: Unused variable.

The messageEndpoint variable is computed but not used in the code that follows. This appears to be leftover code from a refactoring.

Remove the unused variable:

-	messageEndpoint := fmt.Sprintf("%s?sessionId=%s", s.CompleteMessageEndpoint(), sessionID)
-
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9faca53 and 103410e.

📒 Files selected for processing (1)
  • server/sse.go (5 hunks)
🔇 Additional comments (4)
server/sse.go (4)

13-13: Good addition of required time package.

Adding the time package is necessary for the new keep-alive functionality using time.Duration.


66-67: Well-structured addition of keep-alive configuration fields.

The new fields are well-named and follow the existing code style. Having separate fields for the flag and interval provides good flexibility.


144-148: Good implementation of WithKeepAlive option.

The implementation correctly toggles the keep-alive flag without modifying other settings.


165-166: Good default values for keep-alive settings.

The defaults are sensible: keep-alive is disabled by default, and the 10-second interval is a reasonable default when enabled.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🔭 Outside diff range comments (1)
server/sse.go (1)

3-17: 🛠️ Refactor suggestion

Add log package to imports.

The recommended implementation for the keep-alive functionality uses log.Printf() for error handling, but the "log" package is not imported.

import (
	"context"
	"encoding/json"
	"fmt"
+	"log"
	"net/http"
	"net/http/httptest"
	"net/url"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/google/uuid"
	"github.com/mark3labs/mcp-go/mcp"
)
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 103410e and 9a44d14.

📒 Files selected for processing (1)
  • server/sse.go (5 hunks)
🧰 Additional context used
🪛 golangci-lint (1.64.8)
server/sse.go

278-278: expected statement, found 'import'

(typecheck)

🔇 Additional comments (3)
server/sse.go (3)

66-67: Properly added keep-alive configuration fields to SSEServer struct.

These fields will enable the server to maintain active connections by sending periodic ping messages, which is essential when running behind load balancers.


144-148: LGTM - WithKeepAlive option is well implemented.

This option allows users to explicitly enable or disable the keep-alive feature.


165-166: LGTM - Good default values for keep-alive fields.

Setting keepAlive to false by default ensures backward compatibility, and the 10-second interval is a reasonable default for keep-alive messages.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
server/sse.go (2)

137-142: Inconsistent behavior in WithKeepAliveInterval method.

The method automatically sets keepAlive to true even if the user has explicitly disabled it with WithKeepAlive(false). This creates an unintuitive API behavior where the order of option application affects the outcome.

func WithKeepAliveInterval(keepAliveInterval time.Duration) SSEOption {
 	return func(s *SSEServer) {
-		s.keepAlive = true
 		s.keepAliveInterval = keepAliveInterval
 	}
}

277-294: ⚠️ Potential issue

Add error handling for dropped keep-alive pings.

The current implementation doesn't handle the case when the event queue is full, unlike how notification events are handled elsewhere in the code. Additionally, there's a comment that appears to be debug/example output that should be removed.

if s.keepAlive {
	go func() {
		ticker := time.NewTicker(s.keepAliveInterval)
		defer ticker.Stop()
		for {
			select {
			case <-ticker.C:
-				//: ping - 2025-03-27 07:44:38.682659+00:00
-				session.eventQueue <- fmt.Sprintf(":ping - %s\n\n", time.Now().Format(time.RFC3339))
+				select {
+				case session.eventQueue <- fmt.Sprintf(":ping - %s\n\n", time.Now().Format(time.RFC3339)):
+					// Ping sent successfully
+				default:
+					// Queue is full, could log this or take appropriate action
+				}
			case <-session.done:
				return
			case <-r.Context().Done():
				return
			}
		}
	}()
}
🧹 Nitpick comments (1)
server/sse.go (1)

296-296: Remove extra blank line.

This extra blank line doesn't serve any purpose and should be removed for consistency.

	}

-

	// Send the initial endpoint event
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0f5c0af and 15204ed.

📒 Files selected for processing (1)
  • server/sse.go (5 hunks)
🔇 Additional comments (4)
server/sse.go (4)

66-67: New fields added for SSE keep-alive functionality.

These new fields enable the server to send periodic "ping" events to prevent connection timeout, which is essential when running behind load balancers as mentioned in the PR objectives.


144-148: LGTM: WithKeepAlive implementation is correct.

The implementation simply sets the keepAlive field to the provided value, giving users control over whether to enable or disable the keep-alive functionality.


165-167: LGTM: Appropriate default values for keep-alive configuration.

The default settings (keepAlive: false, keepAliveInterval: 10 seconds) are reasonable. Keep-alive is disabled by default to maintain backward compatibility, and the 10-second interval is a common standard for keep-alive mechanisms.


3-17:

❓ Verification inconclusive

Consider adding import for log package if implementing error logging.

If you decide to add error logging for dropped keep-alive pings as suggested in other comments, you'll need to import the "log" package.


🏁 Script executed:

#!/bin/bash
# Check if log package is used elsewhere in the codebase
rg "import\s+\([^)]*\"log\"" server/

Length of output: 36


Action: Verify Error Logging Implementation
The current scan confirms that no file in the server/ directory is importing the "log" package. If you plan to implement error logging (for instance, to log dropped keep-alive pings), please remember to add this import. If error logging isn’t required at this time, you can safely ignore this suggestion.

To further verify, please run the following command and share its output if needed:

#!/bin/bash
# Search for any "log" package import across all Go files to confirm whether it's used elsewhere.
rg --type=go 'import\s*\(.*?"log"' .

@ezynda3 ezynda3 merged commit cc777fc into mark3labs:main Apr 11, 2025
2 checks passed
@rwjblue-glean
Copy link
Collaborator

@lcgash - I just sent #169 that updates the way keep alive works to follow the spec's Base Protocol -> Utilities -> Ping definition. Mind taking a look?

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.

5 participants