@@ -19,8 +19,9 @@ const (
1919 sseVGPURestartTopicSuffix = "_vgpu_restart"
2020)
2121
22- // sseConfigListener connects to the SSE endpoint and triggers config re-fetch on new events.
23- // It reconnects automatically with exponential backoff.
22+ // sseConfigListener connects to the SSE endpoint for config-update events and
23+ // triggers config re-fetch on new events. It reconnects automatically with
24+ // exponential backoff.
2425func (a * Agent ) sseConfigListener () {
2526 defer a .wg .Done ()
2627
@@ -35,22 +36,52 @@ func (a *Agent) sseConfigListener() {
3536
3637 err := a .listenSSE ()
3738 if err != nil {
38- klog .Warningf ("SSE connection error: %v" , err )
39+ klog .Warningf ("SSE config connection error: %v" , err )
3940 }
4041
41- // Check if context is done before reconnecting
4242 select {
4343 case <- a .ctx .Done ():
4444 return
4545 case <- time .After (backoff ):
4646 }
4747
48- // Exponential backoff
4948 backoff = min (backoff * 2 , sseReconnectMax )
5049 }
5150}
5251
53- // listenSSE opens a single SSE connection and processes events until disconnected.
52+ // sseRestartListener connects to the SSE endpoint for vGPU restart events and
53+ // routes them to the reconciler. It runs on a dedicated connection so that
54+ // messages can be attributed to the restart topic without relying on the
55+ // broker setting the SSE `event:` field (which sse.tensor-fusion.ai does not).
56+ func (a * Agent ) sseRestartListener () {
57+ defer a .wg .Done ()
58+
59+ backoff := sseReconnectMin
60+
61+ for {
62+ select {
63+ case <- a .ctx .Done ():
64+ return
65+ default :
66+ }
67+
68+ err := a .listenSSERestart ()
69+ if err != nil {
70+ klog .Warningf ("SSE restart connection error: %v" , err )
71+ }
72+
73+ select {
74+ case <- a .ctx .Done ():
75+ return
76+ case <- time .After (backoff ):
77+ }
78+
79+ backoff = min (backoff * 2 , sseReconnectMax )
80+ }
81+ }
82+
83+ // listenSSE opens a single SSE connection for config-update events (topic =
84+ // agentID) and triggers a debounced pullConfig on every received frame.
5485func (a * Agent ) listenSSE () error {
5586 ctx , cancel := context .WithCancel (a .ctx )
5687 defer cancel ()
@@ -59,7 +90,11 @@ func (a *Agent) listenSSE() error {
5990 if err != nil {
6091 return err
6192 }
62- req .Header .Set (sseTopicHeader , strings .Join ([]string {a .agentID , a .vgpuRestartTopic ()}, "," ))
93+ // Subscribe only to the config-update topic.
94+ // Restart events are handled by a separate connection (listenSSERestart)
95+ // because the SSE broker does not include the topic name in the frame,
96+ // making it impossible to route events from a multi-topic subscription.
97+ req .Header .Set (sseTopicHeader , a .agentID )
6398 req .Header .Set ("Accept" , "text/event-stream" )
6499 req .Header .Set ("Cache-Control" , "no-cache" )
65100
@@ -71,25 +106,31 @@ func (a *Agent) listenSSE() error {
71106 defer func () { _ = resp .Body .Close () }()
72107
73108 if resp .StatusCode != http .StatusOK {
74- klog .Warningf ("SSE endpoint returned status %d" , resp .StatusCode )
109+ klog .Warningf ("SSE config endpoint returned status %d" , resp .StatusCode )
75110 return nil
76111 }
77112
78- klog .Infof ("SSE connection established: topics =%s,%s " , a .agentID , a . vgpuRestartTopic () )
113+ klog .Infof ("SSE config connection established: topic =%s" , a .agentID )
79114
80115 scanner := bufio .NewScanner (resp .Body )
81116 var debounceTimer * time.Timer
82- var eventType string
83117 var eventDataLines []string
84118
85119 flushEvent := func () {
86120 if len (eventDataLines ) == 0 {
87- eventType = ""
88121 return
89122 }
90- a .handleSSEEvent (eventType , eventDataLines , & debounceTimer )
91- eventType = ""
92123 eventDataLines = nil
124+ // Debounce config pull so event bursts result in one pullConfig call.
125+ if debounceTimer != nil {
126+ debounceTimer .Stop ()
127+ }
128+ debounceTimer = time .AfterFunc (sseDebounceDelay , func () {
129+ klog .Infof ("SSE config event received, triggering config re-fetch" )
130+ if err := a .pullConfig (); err != nil {
131+ klog .Errorf ("Failed to pull config after SSE event: %v" , err )
132+ }
133+ })
93134 }
94135
95136 for scanner .Scan () {
@@ -103,12 +144,9 @@ func (a *Agent) listenSSE() error {
103144 }
104145
105146 line := scanner .Text ()
106-
107147 switch {
108148 case line == "" :
109149 flushEvent ()
110- case strings .HasPrefix (line , "event:" ):
111- eventType = strings .TrimSpace (strings .TrimPrefix (line , "event:" ))
112150 case strings .HasPrefix (line , "data:" ):
113151 eventDataLines = append (eventDataLines , strings .TrimSpace (strings .TrimPrefix (line , "data:" )))
114152 }
@@ -123,30 +161,77 @@ func (a *Agent) listenSSE() error {
123161 return err
124162 }
125163
126- klog .Infof ("SSE connection closed by server, will reconnect" )
164+ klog .Infof ("SSE config connection closed by server, will reconnect" )
127165 return nil
128166}
129167
130- func (a * Agent ) vgpuRestartTopic () string {
131- return a .agentID + sseVGPURestartTopicSuffix
132- }
168+ // listenSSERestart opens a single SSE connection for vGPU restart events
169+ // (topic = agentID + "_vgpu_restart"). Every received frame is forwarded
170+ // directly to handleVGPURestartEvent.
171+ func (a * Agent ) listenSSERestart () error {
172+ ctx , cancel := context .WithCancel (a .ctx )
173+ defer cancel ()
133174
134- func (a * Agent ) handleSSEEvent (eventType string , dataLines []string , debounceTimer * * time.Timer ) {
135- if strings .TrimSpace (eventType ) == a .vgpuRestartTopic () {
136- a .handleVGPURestartEvent (dataLines )
137- return
175+ req , err := http .NewRequestWithContext (ctx , http .MethodGet , sseEndpoint , nil )
176+ if err != nil {
177+ return err
138178 }
179+ req .Header .Set (sseTopicHeader , a .vgpuRestartTopic ())
180+ req .Header .Set ("Accept" , "text/event-stream" )
181+ req .Header .Set ("Cache-Control" , "no-cache" )
139182
140- // Debounce config pull so event bursts result in one pullConfig call.
141- if * debounceTimer != nil {
142- (* debounceTimer ).Stop ()
183+ client := & http.Client {}
184+ resp , err := client .Do (req )
185+ if err != nil {
186+ return err
143187 }
144- * debounceTimer = time .AfterFunc (sseDebounceDelay , func () {
145- klog .Infof ("SSE event received, triggering config re-fetch" )
146- if err := a .pullConfig (); err != nil {
147- klog .Errorf ("Failed to pull config after SSE event: %v" , err )
188+ defer func () { _ = resp .Body .Close () }()
189+
190+ if resp .StatusCode != http .StatusOK {
191+ klog .Warningf ("SSE restart endpoint returned status %d" , resp .StatusCode )
192+ return nil
193+ }
194+
195+ klog .Infof ("SSE restart connection established: topic=%s" , a .vgpuRestartTopic ())
196+
197+ scanner := bufio .NewScanner (resp .Body )
198+ var eventDataLines []string
199+
200+ flushEvent := func () {
201+ if len (eventDataLines ) == 0 {
202+ return
203+ }
204+ a .handleVGPURestartEvent (eventDataLines )
205+ eventDataLines = nil
206+ }
207+
208+ for scanner .Scan () {
209+ select {
210+ case <- ctx .Done ():
211+ return nil
212+ default :
148213 }
149- })
214+
215+ line := scanner .Text ()
216+ switch {
217+ case line == "" :
218+ flushEvent ()
219+ case strings .HasPrefix (line , "data:" ):
220+ eventDataLines = append (eventDataLines , strings .TrimSpace (strings .TrimPrefix (line , "data:" )))
221+ }
222+ }
223+ flushEvent ()
224+
225+ if err := scanner .Err (); err != nil {
226+ return err
227+ }
228+
229+ klog .Infof ("SSE restart connection closed by server, will reconnect" )
230+ return nil
231+ }
232+
233+ func (a * Agent ) vgpuRestartTopic () string {
234+ return a .agentID + sseVGPURestartTopicSuffix
150235}
151236
152237func (a * Agent ) handleVGPURestartEvent (dataLines []string ) int {
0 commit comments