Skip to content

feat: add optional JSON-RPC ping keepalive support - #99

Open
bytethm wants to merge 1 commit into
trpc-group:mainfrom
bytethm:sse-keepalive
Open

feat: add optional JSON-RPC ping keepalive support#99
bytethm wants to merge 1 commit into
trpc-group:mainfrom
bytethm:sse-keepalive

Conversation

@bytethm

@bytethm bytethm commented Jan 29, 2026

Copy link
Copy Markdown
Collaborator

Add optional JSON-RPC ping keepalive mechanism for SSE and Streamable HTTP connections, complementing the existing SSE comment keepalive.

Key changes:

  • Add ping configuration options (WithServerPingKeepAlive, WithPingInterval, WithPingTimeout)
  • Implement PingSession and PingAllSessions methods for SSE Server
  • Add automatic ping request handling in SSE Client
  • Support ping keepalive for Streamable HTTP GET SSE connections
  • Add comprehensive tests for backward compatibility and new features
  • Update documentation with configuration examples

The new ping keepalive is disabled by default to maintain backward compatibility. Users can opt-in by using WithServerPingKeepAlive(true).

Ping failures are logged but do not disconnect sessions, ensuring robust operation in unstable network conditions.

由 Sourcery 提供的摘要

在现有的 SSE 注释(comment)保活机制基础上,为核心 SSE 和可流式(Streamable)HTTP 传输新增可配置的基于 JSON-RPC ping 的保活机制,同时为保持向后兼容,默认仍关闭 ping。

新功能:

  • 引入服务器级配置选项,用于控制 SSE 注释保活与 JSON-RPC ping 保活的间隔与超时时间。
  • 为 SSE 服务器添加 JSON-RPC ping 保活支持,包括 PingSessionPingAllSessions 以及自动 ping 循环。
  • 为可流式 HTTP GET SSE 连接添加 JSON-RPC ping 保活支持,包括针对单个会话的 ping 和共享的 ping 循环。
  • 使 SSE 客户端能够自动处理并响应来自服务器的 JSON-RPC ping 请求。

增强:

  • 扩展服务器配置的传递逻辑,使 HTTP 传输层也能接收保活和 ping 设置,从而在不同传输方式之间实现统一行为。

文档:

  • 在 README 中记录 SSE 保活行为、配置选项,以及仅注释模式、仅 ping 模式和二者组合模式的示例配置。

测试:

  • 新增测试,用于验证默认保活和 ping 配置、新增的与 ping 相关的服务器选项,以及与现有 SSE 和服务器配置的向后兼容性。
Original summary in English

Summary by Sourcery

Add configurable JSON-RPC ping-based keepalive alongside existing SSE comment keepalive for both core SSE and Streamable HTTP transports, while keeping ping disabled by default for backward compatibility.

New Features:

  • Introduce server-level configuration options to control SSE comment keepalive and JSON-RPC ping keepalive intervals and timeouts.
  • Add JSON-RPC ping keepalive support to the SSE server, including PingSession, PingAllSessions, and an automatic ping loop.
  • Add JSON-RPC ping keepalive support to Streamable HTTP GET SSE connections, including per-session pings and a shared ping loop.
  • Enable SSE clients to automatically handle and respond to JSON-RPC ping requests from the server.

Enhancements:

  • Extend server configuration wiring so HTTP transport receives keepalive and ping settings for unified behavior across transports.

Documentation:

  • Document SSE keepalive behaviors, configuration options, and example setups for comment-only, ping-only, and combined modes in the README.

Tests:

  • Add tests validating default keepalive and ping configuration, new ping-related server options, and backward compatibility with existing SSE and server configurations.

Add optional JSON-RPC ping keepalive mechanism for SSE and Streamable HTTP
connections, complementing the existing SSE comment keepalive.

Key changes:
- Add ping configuration options (WithServerPingKeepAlive, WithPingInterval, WithPingTimeout)
- Implement PingSession and PingAllSessions methods for SSE Server
- Add automatic ping request handling in SSE Client
- Support ping keepalive for Streamable HTTP GET SSE connections
- Add comprehensive tests for backward compatibility and new features
- Update documentation with configuration examples

The new ping keepalive is disabled by default to maintain backward
compatibility. Users can opt-in by using WithServerPingKeepAlive(true).

Ping failures are logged but do not disconnect sessions, ensuring robust
operation in unstable network conditions.
@sourcery-ai

sourcery-ai Bot commented Jan 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

在现有 SSE 注释式 keepalive 的基础上,新增可选的基于 JSON-RPC ping 的 keepalive 机制;通过顶层 Server API 将其配置传递到 SSE 服务器和可流式的 HTTP 处理器中;实现服务端的 ping 循环和基于会话的 ping 辅助方法;更新 SSE 客户端以响应 ping 请求;并为新行为及向后兼容性编写文档和测试。

基于 SSE 的 JSON-RPC ping keepalive 时序图

sequenceDiagram
    participant SSEServer
    participant PingLoop
    participant Client as sseClientTransport

    SSEServer->>SSEServer: handleSSE
    alt pingEnabled
        SSEServer->>SSEServer: startPingLoop
        activate PingLoop
        loop every pingInterval
            PingLoop->>SSEServer: PingAllSessions(ctx)
            activate SSEServer
            SSEServer->>SSEServer: Range over sessions
            par for each sessionID
                SSEServer->>SSEServer: PingSession(pingCtx, sessionID)
                activate SSEServer
                SSEServer->>Client: SendRequest(ctx, sessionID, JSONRPC ping)
                activate Client
                Client->>Client: handleIncomingRequest(data)
                Client->>Client: handlePingRequest(request)
                Client-->>SSEServer: JSONRPCResponse (empty result)
                deactivate Client
                SSEServer->>SSEServer: verify result not nil
                deactivate SSEServer
            and for each sessionID
            end
            SSEServer-->>PingLoop: all pings done or timeout
            deactivate SSEServer
        end
        deactivate PingLoop
    else ping disabled
        SSEServer->>SSEServer: no ping loop started
    end

    SSEServer->>SSEServer: Close
    SSEServer->>PingLoop: close stop channel
    PingLoop-->>SSEServer: ping loop exits
Loading

流式 HTTP GET SSE 的 JSON-RPC ping keepalive 时序图

sequenceDiagram
    participant HTTPHandler as httpServerHandler
    participant PingLoop
    participant Client as sseClientTransport

    HTTPHandler->>HTTPHandler: handleGet
    alt pingEnabled
        HTTPHandler->>HTTPHandler: pingStartMu.Lock
        alt !pingStarted
            HTTPHandler->>HTTPHandler: pingStarted = true
            HTTPHandler->>HTTPHandler: pingStartMu.Unlock
            HTTPHandler->>HTTPHandler: startPingLoop
            activate PingLoop
        else ping already started
            HTTPHandler->>HTTPHandler: pingStartMu.Unlock
        end
    end

    alt keepAliveEnabled
        HTTPHandler->>HTTPHandler: handleGetSSECommentKeepAlive
        loop every keepAliveInterval
            HTTPHandler->>Client: send ": keepalive" SSE comment
        end
    end

    loop every pingInterval
        PingLoop->>HTTPHandler: PingAllSessions(ctx)
        activate HTTPHandler
        HTTPHandler->>HTTPHandler: collect getSSEConnections sessionIDs
        par for each sessionID
            HTTPHandler->>HTTPHandler: PingSession(pingCtx, sessionID)
            activate HTTPHandler
            HTTPHandler->>Client: SendRequest(ctx, sessionID, JSONRPC ping)
            activate Client
            Client->>Client: handleIncomingRequest(data)
            Client->>Client: handlePingRequest(request)
            Client-->>HTTPHandler: JSONRPCResponse (empty result)
            deactivate Client
            HTTPHandler->>HTTPHandler: verify result not nil
            deactivate HTTPHandler
        and for each sessionID
        end
        HTTPHandler-->>PingLoop: all pings done or timeout
        deactivate HTTPHandler
    end

    HTTPHandler->>HTTPHandler: Close
    HTTPHandler->>PingLoop: close stop channel
    PingLoop-->>HTTPHandler: ping loop exits
Loading

服务器、SSE 服务器、HTTP 处理器和 SSE 客户端 ping keepalive 的更新类图

classDiagram
    class Server {
        +serverConfig config
        +NewServer(name string, version string, options ...ServerOption)
        +initComponents()
    }

    class serverConfig {
        +bool postSSEEnabled
        +bool getSSEEnabled
        +int notificationBufferSize
        +bool keepAliveEnabled
        +time_Duration keepAliveInterval
        +bool pingEnabled
        +time_Duration pingInterval
        +time_Duration pingTimeout
    }

    class SSEServer {
        +bool keepAlive
        +time_Duration keepAliveInterval
        +bool pingEnabled
        +time_Duration pingInterval
        +time_Duration pingTimeout
        +bool pingStarted
        +sync_Mutex pingStartMu
        +chan_struct stop
        +sync_Once stopOnce
        +Logger logger
        +atomic_Int64 requestID
        +PingSession(ctx context_Context, sessionID string) error
        +PingAllSessions(ctx context_Context)
        +startPingLoop()
        +Close() error
    }

    class httpServerHandler {
        +requestHandler handler
        +string serverPath
        +responseManager responseManager
        +bool keepAliveEnabled
        +time_Duration keepAliveInterval
        +bool pingEnabled
        +time_Duration pingInterval
        +time_Duration pingTimeout
        +bool pingStarted
        +sync_Mutex pingStartMu
        +chan_struct stop
        +sync_Once stopOnce
        +handleGet(ctx context_Context, w http_ResponseWriter, r *http_Request)
        +handleGetSSECommentKeepAlive(ctx context_Context, conn *getSSEConnection, sessionID string)
        +PingSession(ctx context_Context, sessionID string) error
        +PingAllSessions(ctx context_Context)
        +startPingLoop()
        +Close() error
    }

    class sseClientTransport {
        +Logger logger
        +handleIncomingRequest(data string)
        +handleRootsListRequest(request *JSONRPCRequest)
        +handlePingRequest(request *JSONRPCRequest)
        +sendResponseMessage(response *JSONRPCResponse)
        +sendErrorResponse(request *JSONRPCRequest, code int, message string)
    }

    class ServerOption {
    }

    class SSEOption {
    }

    %% Server options wiring keepalive and ping configuration
    class WithServerKeepAlive {
        +WithServerKeepAlive(enabled bool) ServerOption
    }

    class WithServerKeepAliveInterval {
        +WithServerKeepAliveInterval(interval time_Duration) ServerOption
    }

    class WithServerPingKeepAlive {
        +WithServerPingKeepAlive(enabled bool) ServerOption
    }

    class WithServerPingInterval {
        +WithServerPingInterval(interval time_Duration) ServerOption
    }

    class WithServerPingTimeout {
        +WithServerPingTimeout(timeout time_Duration) ServerOption
    }

    %% SSE server options
    class WithKeepAliveInterval {
        +WithKeepAliveInterval(interval time_Duration) SSEOption
    }

    class WithPingKeepAlive {
        +WithPingKeepAlive(enabled bool) SSEOption
    }

    class WithPingInterval {
        +WithPingInterval(interval time_Duration) SSEOption
    }

    class WithPingTimeout {
        +WithPingTimeout(timeout time_Duration) SSEOption
    }

    %% HTTP transport internal options
    class withKeepAliveConfig {
        +withKeepAliveConfig(enabled bool, interval time_Duration) func_httpServerHandler
    }

    class withPingConfig {
        +withPingConfig(enabled bool, interval time_Duration, timeout time_Duration) func_httpServerHandler
    }

    Server *-- serverConfig
    Server --> SSEServer : uses
    Server --> httpServerHandler : uses

    ServerOption <|-- WithServerKeepAlive
    ServerOption <|-- WithServerKeepAliveInterval
    ServerOption <|-- WithServerPingKeepAlive
    ServerOption <|-- WithServerPingInterval
    ServerOption <|-- WithServerPingTimeout

    SSEOption <|-- WithKeepAliveInterval
    SSEOption <|-- WithPingKeepAlive
    SSEOption <|-- WithPingInterval
    SSEOption <|-- WithPingTimeout

    httpServerHandler ..> withKeepAliveConfig : configured_by
    httpServerHandler ..> withPingConfig : configured_by

    SSEServer ..> sseClientTransport : sends_JSONRPC_ping
    httpServerHandler ..> sseClientTransport : sends_JSONRPC_ping
    sseClientTransport ..> SSEServer : responds_to_ping
    sseClientTransport ..> httpServerHandler : responds_to_ping
Loading

文件级变更

Change Details Files
将 keepalive 和 ping 配置引入顶层 Server,并将其传递到 HTTP 流式处理器。
  • 扩展 serverConfig,增加 keepAliveEnabled/Interval 和 pingEnabled/Interval/Timeout 字段,设置合理默认值以保持现有行为。
  • 在构造 httpServerHandler 时,通过 withKeepAliveConfig 和 withPingConfig 选项,将新的 keepalive 和 ping 配置接入 initComponents。
  • 暴露新的 Server 选项 WithServerKeepAlive、WithServerKeepAliveInterval、WithServerPingKeepAlive、WithServerPingInterval 和 WithServerPingTimeout,并通过单元测试覆盖这些选项及向后兼容场景。
server.go
streamable_server.go
server_test.go
为可流式 HTTP GET SSE 连接增加 ping keepalive 支持和 SSE 注释 keepalive。
  • 为 httpServerHandler 增加 ping 和 keepalive 状态(间隔、标志、停止通道、一次性启动锁/标志)。
  • 在第一个 GET SSE 连接上,按需启动后台 ping 循环,周期性调用 PingAllSessions,并对每个会话使用超时控制;通过 Close 使用 stop channel 停止循环。
  • 实现 handleGetSSECommentKeepAlive,当启用时在每个 GET SSE 连接上周期性写入 SSE 注释 keepalive 帧。
  • 新增 PingSession 和 PingAllSessions 辅助方法,通过现有会话发送 JSON-RPC MethodPing 请求,并在不关闭会话的情况下记录失败日志。
streamable_server.go
为 SSEServer 引入 JSON-RPC ping keepalive,包括配置选项和 ping 循环。
  • 扩展 SSEServer,加入 pingEnabled/Interval/Timeout 字段、pingStarted 标志、启动互斥锁,以及用于生命周期管理的 stop channel/once,并在 NewSSEServer 中初始化默认值。
  • 新增 SSE 选项 WithPingKeepAlive、WithPingInterval 和 WithPingTimeout,使 ping 行为可以独立于注释式 keepalive 进行控制。
  • 在第一个 SSE 连接时,启动后台 ping 循环(startPingLoop),周期性调用 PingAllSessions,并为整个循环设置超时,在服务器 Close 时退出。
  • 实现 PingSession 和 PingAllSessions 辅助方法,发送 JSON-RPC MethodPing 请求,对每个会话使用超时控制,并在不断开会话的情况下记录失败日志。
  • 新增测试,用于验证默认 ping 配置、与现有 keepalive 选项的交互,以及 SSEServer 行为整体的向后兼容性。
sse_server.go
sse_server_test.go
让 SSE 客户端能够处理服务器发起的 JSON-RPC ping 请求。
  • 扩展 handleIncomingRequest 的 switch 以处理 MethodPing。
  • 实现 handlePingRequest,记录收到请求的日志,发送一个具有相同 ID 的空对象 JSON-RPC 响应,并记录完成日志。
sse_client.go
更新文档,描述 SSE 注释 keepalive 和 JSON-RPC ping keepalive,并暴露新的服务器选项。
  • 在配置表中记录新的服务器选项(WithServerKeepAlive、WithServerKeepAliveInterval、WithServerPingKeepAlive、WithServerPingInterval、WithServerPingTimeout)。
  • 新增“Connection Keepalive”章节,解释这两种机制、各自的权衡,并给出示例配置,包括默认配置、双模式、仅 ping、以及自定义间隔等,同时引用一个示例目录。
README.md

Tips and commands

Interacting with Sourcery

  • 触发新的审查: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 在回复某条审查评论时请求 Sourcery 从该评论创建 issue。你也可以直接回复审查评论 @sourcery-ai issue 来从中创建 issue。
  • 生成 pull request 标题: 在 pull request 标题任意位置写入 @sourcery-ai 以在任意时刻生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 总结: 在 pull request 正文任意位置写入 @sourcery-ai summary,即可在该位置生成 PR 总结。你也可以在 pull request 中评论 @sourcery-ai summary 来(重新)生成总结。
  • 生成审查指南: 在 pull request 中评论 @sourcery-ai guide,即可在任意时间(重新)生成审查者指南。
  • 解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve 以标记解决所有 Sourcery 评论。如果你已经处理完这些评论且不希望再看到它们,这会很有用。
  • 忽略所有 Sourcery 审查: 在 pull request 中评论 @sourcery-ai dismiss 以忽略所有现有 Sourcery 审查。若你想从一次新的审查开始,这尤其有用——别忘了再评论 @sourcery-ai review 来触发新的审查!

Customizing Your Experience

访问你的 dashboard 来:

  • 启用或禁用审查功能,例如 Sourcery 生成的 pull request 总结、审查者指南等。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查指令。
  • 调整其他审查设置。

Getting Help

Original review guide in English

Reviewer's Guide

Adds an optional JSON-RPC ping-based keepalive alongside existing SSE comment keepalive, wires its configuration through the top-level Server API into both the SSE server and streamable HTTP handler, implements server-side ping loops and per-session ping helpers, updates the SSE client to answer ping requests, and documents and tests the new behavior and backward compatibility.

Sequence diagram for JSON-RPC ping keepalive over SSE

sequenceDiagram
    participant SSEServer
    participant PingLoop
    participant Client as sseClientTransport

    SSEServer->>SSEServer: handleSSE
    alt pingEnabled
        SSEServer->>SSEServer: startPingLoop
        activate PingLoop
        loop every pingInterval
            PingLoop->>SSEServer: PingAllSessions(ctx)
            activate SSEServer
            SSEServer->>SSEServer: Range over sessions
            par for each sessionID
                SSEServer->>SSEServer: PingSession(pingCtx, sessionID)
                activate SSEServer
                SSEServer->>Client: SendRequest(ctx, sessionID, JSONRPC ping)
                activate Client
                Client->>Client: handleIncomingRequest(data)
                Client->>Client: handlePingRequest(request)
                Client-->>SSEServer: JSONRPCResponse (empty result)
                deactivate Client
                SSEServer->>SSEServer: verify result not nil
                deactivate SSEServer
            and for each sessionID
            end
            SSEServer-->>PingLoop: all pings done or timeout
            deactivate SSEServer
        end
        deactivate PingLoop
    else ping disabled
        SSEServer->>SSEServer: no ping loop started
    end

    SSEServer->>SSEServer: Close
    SSEServer->>PingLoop: close stop channel
    PingLoop-->>SSEServer: ping loop exits
Loading

Sequence diagram for JSON-RPC ping keepalive for Streamable HTTP GET SSE

sequenceDiagram
    participant HTTPHandler as httpServerHandler
    participant PingLoop
    participant Client as sseClientTransport

    HTTPHandler->>HTTPHandler: handleGet
    alt pingEnabled
        HTTPHandler->>HTTPHandler: pingStartMu.Lock
        alt !pingStarted
            HTTPHandler->>HTTPHandler: pingStarted = true
            HTTPHandler->>HTTPHandler: pingStartMu.Unlock
            HTTPHandler->>HTTPHandler: startPingLoop
            activate PingLoop
        else ping already started
            HTTPHandler->>HTTPHandler: pingStartMu.Unlock
        end
    end

    alt keepAliveEnabled
        HTTPHandler->>HTTPHandler: handleGetSSECommentKeepAlive
        loop every keepAliveInterval
            HTTPHandler->>Client: send ": keepalive" SSE comment
        end
    end

    loop every pingInterval
        PingLoop->>HTTPHandler: PingAllSessions(ctx)
        activate HTTPHandler
        HTTPHandler->>HTTPHandler: collect getSSEConnections sessionIDs
        par for each sessionID
            HTTPHandler->>HTTPHandler: PingSession(pingCtx, sessionID)
            activate HTTPHandler
            HTTPHandler->>Client: SendRequest(ctx, sessionID, JSONRPC ping)
            activate Client
            Client->>Client: handleIncomingRequest(data)
            Client->>Client: handlePingRequest(request)
            Client-->>HTTPHandler: JSONRPCResponse (empty result)
            deactivate Client
            HTTPHandler->>HTTPHandler: verify result not nil
            deactivate HTTPHandler
        and for each sessionID
        end
        HTTPHandler-->>PingLoop: all pings done or timeout
        deactivate HTTPHandler
    end

    HTTPHandler->>HTTPHandler: Close
    HTTPHandler->>PingLoop: close stop channel
    PingLoop-->>HTTPHandler: ping loop exits
Loading

Updated class diagram for server, SSE server, HTTP handler, and SSE client ping keepalive

classDiagram
    class Server {
        +serverConfig config
        +NewServer(name string, version string, options ...ServerOption)
        +initComponents()
    }

    class serverConfig {
        +bool postSSEEnabled
        +bool getSSEEnabled
        +int notificationBufferSize
        +bool keepAliveEnabled
        +time_Duration keepAliveInterval
        +bool pingEnabled
        +time_Duration pingInterval
        +time_Duration pingTimeout
    }

    class SSEServer {
        +bool keepAlive
        +time_Duration keepAliveInterval
        +bool pingEnabled
        +time_Duration pingInterval
        +time_Duration pingTimeout
        +bool pingStarted
        +sync_Mutex pingStartMu
        +chan_struct stop
        +sync_Once stopOnce
        +Logger logger
        +atomic_Int64 requestID
        +PingSession(ctx context_Context, sessionID string) error
        +PingAllSessions(ctx context_Context)
        +startPingLoop()
        +Close() error
    }

    class httpServerHandler {
        +requestHandler handler
        +string serverPath
        +responseManager responseManager
        +bool keepAliveEnabled
        +time_Duration keepAliveInterval
        +bool pingEnabled
        +time_Duration pingInterval
        +time_Duration pingTimeout
        +bool pingStarted
        +sync_Mutex pingStartMu
        +chan_struct stop
        +sync_Once stopOnce
        +handleGet(ctx context_Context, w http_ResponseWriter, r *http_Request)
        +handleGetSSECommentKeepAlive(ctx context_Context, conn *getSSEConnection, sessionID string)
        +PingSession(ctx context_Context, sessionID string) error
        +PingAllSessions(ctx context_Context)
        +startPingLoop()
        +Close() error
    }

    class sseClientTransport {
        +Logger logger
        +handleIncomingRequest(data string)
        +handleRootsListRequest(request *JSONRPCRequest)
        +handlePingRequest(request *JSONRPCRequest)
        +sendResponseMessage(response *JSONRPCResponse)
        +sendErrorResponse(request *JSONRPCRequest, code int, message string)
    }

    class ServerOption {
    }

    class SSEOption {
    }

    %% Server options wiring keepalive and ping configuration
    class WithServerKeepAlive {
        +WithServerKeepAlive(enabled bool) ServerOption
    }

    class WithServerKeepAliveInterval {
        +WithServerKeepAliveInterval(interval time_Duration) ServerOption
    }

    class WithServerPingKeepAlive {
        +WithServerPingKeepAlive(enabled bool) ServerOption
    }

    class WithServerPingInterval {
        +WithServerPingInterval(interval time_Duration) ServerOption
    }

    class WithServerPingTimeout {
        +WithServerPingTimeout(timeout time_Duration) ServerOption
    }

    %% SSE server options
    class WithKeepAliveInterval {
        +WithKeepAliveInterval(interval time_Duration) SSEOption
    }

    class WithPingKeepAlive {
        +WithPingKeepAlive(enabled bool) SSEOption
    }

    class WithPingInterval {
        +WithPingInterval(interval time_Duration) SSEOption
    }

    class WithPingTimeout {
        +WithPingTimeout(timeout time_Duration) SSEOption
    }

    %% HTTP transport internal options
    class withKeepAliveConfig {
        +withKeepAliveConfig(enabled bool, interval time_Duration) func_httpServerHandler
    }

    class withPingConfig {
        +withPingConfig(enabled bool, interval time_Duration, timeout time_Duration) func_httpServerHandler
    }

    Server *-- serverConfig
    Server --> SSEServer : uses
    Server --> httpServerHandler : uses

    ServerOption <|-- WithServerKeepAlive
    ServerOption <|-- WithServerKeepAliveInterval
    ServerOption <|-- WithServerPingKeepAlive
    ServerOption <|-- WithServerPingInterval
    ServerOption <|-- WithServerPingTimeout

    SSEOption <|-- WithKeepAliveInterval
    SSEOption <|-- WithPingKeepAlive
    SSEOption <|-- WithPingInterval
    SSEOption <|-- WithPingTimeout

    httpServerHandler ..> withKeepAliveConfig : configured_by
    httpServerHandler ..> withPingConfig : configured_by

    SSEServer ..> sseClientTransport : sends_JSONRPC_ping
    httpServerHandler ..> sseClientTransport : sends_JSONRPC_ping
    sseClientTransport ..> SSEServer : responds_to_ping
    sseClientTransport ..> httpServerHandler : responds_to_ping
Loading

File-Level Changes

Change Details Files
Introduce keepalive and ping configuration to the top-level Server and propagate it into the HTTP streamable handler.
  • Extend serverConfig with keepAliveEnabled/Interval and pingEnabled/Interval/Timeout fields with sensible defaults preserving existing behavior.
  • Wire new keepalive and ping configuration into initComponents via withKeepAliveConfig and withPingConfig options when constructing the httpServerHandler.
  • Expose new Server options WithServerKeepAlive, WithServerKeepAliveInterval, WithServerPingKeepAlive, WithServerPingInterval, and WithServerPingTimeout, and cover them with unit tests including backward-compat scenarios.
server.go
streamable_server.go
server_test.go
Add ping keepalive support and SSE comment keepalive for Streamable HTTP GET SSE connections.
  • Augment httpServerHandler with ping and keepalive state (intervals, flags, stop channel, one-time start mutex/flag).
  • On first GET SSE connection, conditionally start a background ping loop that periodically calls PingAllSessions with per-session timeouts, and stop it via Close using a stop channel.
  • Implement handleGetSSECommentKeepAlive to periodically write SSE comment keepalive frames on each GET SSE connection when enabled.
  • Add PingSession and PingAllSessions helpers that send JSON-RPC MethodPing requests over existing sessions and log failures without closing sessions.
streamable_server.go
Introduce JSON-RPC ping keepalive for the SSEServer, including configuration options and ping loop.
  • Extend SSEServer with pingEnabled/Interval/Timeout fields, pingStarted flag, start mutex, and stop channel/once for lifecycle management, initializing defaults in NewSSEServer.
  • Add SSE options WithPingKeepAlive, WithPingInterval, and WithPingTimeout to control ping behavior independently of comment keepalive.
  • On first SSE connection, start a background ping loop (startPingLoop) that periodically calls PingAllSessions with an overall cycle timeout, exiting on server Close.
  • Implement PingSession and PingAllSessions helpers that send JSON-RPC MethodPing requests, use per-session timeouts, and log failures without disconnecting sessions.
  • Add tests that validate default ping config, interaction with existing keepalive options, and overall backward compatibility of SSEServer behavior.
sse_server.go
sse_server_test.go
Teach the SSE client to handle server-initiated JSON-RPC ping requests.
  • Extend handleIncomingRequest switch to handle MethodPing.
  • Implement handlePingRequest to log receipt, send an empty-object JSON-RPC response with the same ID, and log completion.
sse_client.go
Update documentation to describe both SSE comment and JSON-RPC ping keepalive and expose new server options.
  • Document new server options (WithServerKeepAlive, WithServerKeepAliveInterval, WithServerPingKeepAlive, WithServerPingInterval, WithServerPingTimeout) in the configuration table.
  • Add a new "Connection Keepalive" section explaining the two mechanisms, their trade-offs, and example configurations including default, both-modes, ping-only, and custom intervals, and reference an example directory.
README.md

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - 我发现了 1 个问题,并给出了一些整体性的反馈:

  • SSEServerhttpServerHandler 中的 ping keepalive 逻辑几乎完全相同(字段、PingSessionPingAllSessionsstartPingLoopClose);可以考虑抽取一个共享的 helper 或类型来减少重复代码,并更容易保持行为一致。
  • handleGetSSECommentKeepAlive 中,对 SSE 连接的写操作(fmt.FprintfFlush)忽略了错误;如果写操作重复失败(例如客户端已断开但尚未被检测到),你可能会希望记录日志和/或提前停止 keepalive 循环,以避免噪声日志或冗余工作。
给 AI 代理的提示
Please address the comments from this code review:

## Overall Comments
- The ping keepalive logic in `SSEServer` and `httpServerHandler` is nearly identical (fields, `PingSession`, `PingAllSessions`, `startPingLoop`, `Close`); consider extracting a shared helper or type to reduce duplication and keep the behavior in sync more easily.
- In `handleGetSSECommentKeepAlive`, writes to the SSE connection (`fmt.Fprintf` and `Flush`) ignore errors; if a write fails repeatedly (e.g., client gone but not yet detected), you may want to log and/or stop the keepalive loop early to avoid noisy logs or redundant work.

## Individual Comments

### Comment 1
<location> `streamable_server.go:698-707` </location>
<code_context>

+// handleGetSSECommentKeepAlive handles SSE comment keepalive for GET SSE connections.
+// It sends SSE comment lines at the configured interval to keep the connection alive.
+func (h *httpServerHandler) handleGetSSECommentKeepAlive(ctx context.Context, conn *getSSEConnection, sessionID string) {
+	if h.keepAliveInterval <= 0 {
+		return
+	}
+
+	ticker := time.NewTicker(h.keepAliveInterval)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ticker.C:
+			conn.writeLock.Lock()
+			// Send SSE comment
</code_context>

<issue_to_address>
**suggestion (bug_risk):** 可以考虑在 SSE keepalive 循环中处理写入/刷新错误,以避免静默忽略已断开的连接。

在 `handleGetSSECommentKeepAlive` 中,`Fprintf``Flush` 的调用没有检查返回的错误。如果客户端断开连接或 writer 失败,keepalive goroutine 会一直运行到上下文被取消,而不是尽快退出。请在写入/刷新失败时处理这些错误(并可选择记录日志),以便循环在写入/刷新失败时能提前终止。

```suggestion
		case <-ticker.C:
			conn.writeLock.Lock()
			// Send SSE comment
			if _, err := fmt.Fprintf(conn.writer, ": keepalive\n\n"); err != nil {
				conn.writeLock.Unlock()
				if h.logger != nil {
					h.logger.Errorf("Failed to send keepalive comment to GET SSE session %s: %v", sessionID, err)
				}
				// Exit keepalive loop on write failure
				return
			}

			conn.flusher.Flush()
			conn.writeLock.Unlock()

			if h.logger != nil {
				h.logger.Debugf("Sent keepalive comment to GET SSE session: %s", sessionID)
			}
```
</issue_to_address>

Sourcery 对开源项目免费——如果你觉得我们的 Review 有帮助,欢迎分享 ✨
帮我变得更有用!请对每条评论点 👍 或 👎,我会根据你的反馈来改进 Review 质量。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • The ping keepalive logic in SSEServer and httpServerHandler is nearly identical (fields, PingSession, PingAllSessions, startPingLoop, Close); consider extracting a shared helper or type to reduce duplication and keep the behavior in sync more easily.
  • In handleGetSSECommentKeepAlive, writes to the SSE connection (fmt.Fprintf and Flush) ignore errors; if a write fails repeatedly (e.g., client gone but not yet detected), you may want to log and/or stop the keepalive loop early to avoid noisy logs or redundant work.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The ping keepalive logic in `SSEServer` and `httpServerHandler` is nearly identical (fields, `PingSession`, `PingAllSessions`, `startPingLoop`, `Close`); consider extracting a shared helper or type to reduce duplication and keep the behavior in sync more easily.
- In `handleGetSSECommentKeepAlive`, writes to the SSE connection (`fmt.Fprintf` and `Flush`) ignore errors; if a write fails repeatedly (e.g., client gone but not yet detected), you may want to log and/or stop the keepalive loop early to avoid noisy logs or redundant work.

## Individual Comments

### Comment 1
<location> `streamable_server.go:698-707` </location>
<code_context>

+// handleGetSSECommentKeepAlive handles SSE comment keepalive for GET SSE connections.
+// It sends SSE comment lines at the configured interval to keep the connection alive.
+func (h *httpServerHandler) handleGetSSECommentKeepAlive(ctx context.Context, conn *getSSEConnection, sessionID string) {
+	if h.keepAliveInterval <= 0 {
+		return
+	}
+
+	ticker := time.NewTicker(h.keepAliveInterval)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ticker.C:
+			conn.writeLock.Lock()
+			// Send SSE comment
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Consider handling write/flush errors in the SSE keepalive loop to avoid silently ignoring broken connections.

In `handleGetSSECommentKeepAlive`, the `Fprintf` and `Flush` calls are executed without checking their return errors. If the client disconnects or the writer fails, the keepalive goroutine will keep running until context cancellation instead of exiting promptly. Please handle these errors (and optionally log them) so the loop can terminate early on write/flush failure.

```suggestion
		case <-ticker.C:
			conn.writeLock.Lock()
			// Send SSE comment
			if _, err := fmt.Fprintf(conn.writer, ": keepalive\n\n"); err != nil {
				conn.writeLock.Unlock()
				if h.logger != nil {
					h.logger.Errorf("Failed to send keepalive comment to GET SSE session %s: %v", sessionID, err)
				}
				// Exit keepalive loop on write failure
				return
			}

			conn.flusher.Flush()
			conn.writeLock.Unlock()

			if h.logger != nil {
				h.logger.Debugf("Sent keepalive comment to GET SSE session: %s", sessionID)
			}
```
</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 thread streamable_server.go
Comment on lines +698 to +707
case <-ticker.C:
conn.writeLock.Lock()
// Send SSE comment
fmt.Fprintf(conn.writer, ": keepalive\n\n")
conn.flusher.Flush()
conn.writeLock.Unlock()

if h.logger != nil {
h.logger.Debugf("Sent keepalive comment to GET SSE session: %s", sessionID)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): 可以考虑在 SSE keepalive 循环中处理写入/刷新错误,以避免静默忽略已断开的连接。

handleGetSSECommentKeepAlive 中,FprintfFlush 的调用没有检查返回的错误。如果客户端断开连接或 writer 失败,keepalive goroutine 会一直运行到上下文被取消,而不是尽快退出。请在写入/刷新失败时处理这些错误(并可选择记录日志),以便循环在写入/刷新失败时能提前终止。

Suggested change
case <-ticker.C:
conn.writeLock.Lock()
// Send SSE comment
fmt.Fprintf(conn.writer, ": keepalive\n\n")
conn.flusher.Flush()
conn.writeLock.Unlock()
if h.logger != nil {
h.logger.Debugf("Sent keepalive comment to GET SSE session: %s", sessionID)
}
case <-ticker.C:
conn.writeLock.Lock()
// Send SSE comment
if _, err := fmt.Fprintf(conn.writer, ": keepalive\n\n"); err != nil {
conn.writeLock.Unlock()
if h.logger != nil {
h.logger.Errorf("Failed to send keepalive comment to GET SSE session %s: %v", sessionID, err)
}
// Exit keepalive loop on write failure
return
}
conn.flusher.Flush()
conn.writeLock.Unlock()
if h.logger != nil {
h.logger.Debugf("Sent keepalive comment to GET SSE session: %s", sessionID)
}
Original comment in English

suggestion (bug_risk): Consider handling write/flush errors in the SSE keepalive loop to avoid silently ignoring broken connections.

In handleGetSSECommentKeepAlive, the Fprintf and Flush calls are executed without checking their return errors. If the client disconnects or the writer fails, the keepalive goroutine will keep running until context cancellation instead of exiting promptly. Please handle these errors (and optionally log them) so the loop can terminate early on write/flush failure.

Suggested change
case <-ticker.C:
conn.writeLock.Lock()
// Send SSE comment
fmt.Fprintf(conn.writer, ": keepalive\n\n")
conn.flusher.Flush()
conn.writeLock.Unlock()
if h.logger != nil {
h.logger.Debugf("Sent keepalive comment to GET SSE session: %s", sessionID)
}
case <-ticker.C:
conn.writeLock.Lock()
// Send SSE comment
if _, err := fmt.Fprintf(conn.writer, ": keepalive\n\n"); err != nil {
conn.writeLock.Unlock()
if h.logger != nil {
h.logger.Errorf("Failed to send keepalive comment to GET SSE session %s: %v", sessionID, err)
}
// Exit keepalive loop on write failure
return
}
conn.flusher.Flush()
conn.writeLock.Unlock()
if h.logger != nil {
h.logger.Debugf("Sent keepalive comment to GET SSE session: %s", sessionID)
}

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