Skip to content

Commit ed889ed

Browse files
authored
feat: add heartbeat and buffered channel to sse handler (#38)
1 parent 5b78ae2 commit ed889ed

2 files changed

Lines changed: 75 additions & 64 deletions

File tree

eventsse/internal/handlers/pub/pub.go

Lines changed: 73 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"net/http"
88
"os"
9+
"time"
910

1011
"github.com/krateoplatformops/eventsse/internal/labels"
1112
"github.com/krateoplatformops/eventsse/internal/store"
@@ -14,82 +15,102 @@ import (
1415
corev1 "k8s.io/api/core/v1"
1516
)
1617

18+
const (
19+
defaultHeartbeatInterval = 25 * time.Second
20+
defaultEventQueueSize = 256
21+
)
22+
1723
func SSE(cli clientv3.Watcher) http.Handler {
18-
return &handler{
19-
cli: cli,
20-
}
24+
return &handler{cli: cli}
2125
}
2226

23-
var _ http.Handler = (*handler)(nil)
24-
2527
type handler struct {
2628
cli clientv3.Watcher
2729
}
2830

29-
// @title EventSSE API
30-
// @version 1.0
31-
// @description This the Krateo EventSSE server.
32-
// @BasePath /
33-
34-
// Health godoc
35-
// @Summary SSE Endpoint
36-
// @Description Get available events notifications
37-
// @ID notifications
38-
// @Produce json
39-
// @Success 200 {array} types.Event
40-
// @Router /pub [get]
41-
func (r *handler) ServeHTTP(wri http.ResponseWriter, req *http.Request) {
31+
func (r *handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
4232
ctx, cancel := context.WithCancel(req.Context())
4333
defer cancel()
4434

45-
// === CORS Preflight ===
35+
// CORS
4636
if req.Method == http.MethodOptions {
47-
r.setCORSHeaders(wri)
48-
wri.WriteHeader(http.StatusNoContent)
37+
r.setCORSHeaders(w)
38+
w.WriteHeader(http.StatusNoContent)
4939
return
5040
}
41+
r.setCORSHeaders(w)
5142

52-
r.setCORSHeaders(wri)
53-
54-
// === SSE Headers ===
55-
wri.Header().Set("Content-Type", "text/event-stream")
56-
wri.Header().Set("Cache-Control", "no-cache")
57-
wri.Header().Set("Connection", "keep-alive")
58-
wri.Header().Set("X-Accel-Buffering", "no")
43+
// SSE headers
44+
w.Header().Set("Content-Type", "text/event-stream")
45+
w.Header().Set("Cache-Control", "no-cache")
46+
w.Header().Set("Connection", "keep-alive")
47+
w.Header().Set("X-Accel-Buffering", "no")
5948

6049
log := zerolog.New(os.Stdout).With().
6150
Str("service", "eventsse").
6251
Timestamp().
6352
Logger()
6453

65-
f, ok := wri.(http.Flusher)
54+
flusher, ok := w.(http.Flusher)
6655
if !ok {
6756
log.Error().Msg("http.ResponseWriter does not implement http.Flusher")
68-
http.Error(wri, "Streaming not supported", http.StatusInternalServerError)
57+
http.Error(w, "Streaming not supported", http.StatusInternalServerError)
58+
return
59+
}
60+
61+
eventCh := make(chan string, defaultEventQueueSize)
62+
defer close(eventCh)
63+
64+
go func() {
65+
for evt := range eventCh {
66+
_, err := w.Write([]byte(evt))
67+
if err != nil {
68+
log.Info().Msg("Client disconnected (write error)")
69+
return
70+
}
71+
flusher.Flush()
72+
}
73+
}()
74+
75+
// Evento iniziale
76+
initial := fmt.Sprintf("event: connection-established\nid: 88888888\ndata: %s\n\n", `{"info": "Ready to watch events"}`)
77+
select {
78+
case eventCh <- initial:
79+
case <-ctx.Done():
6980
return
7081
}
7182

72-
fmt.Fprintln(wri, "event: connection-established")
73-
fmt.Fprintln(wri, "id: 88888888")
74-
fmt.Fprintf(wri, "data: %s\n\n", `{"info": "Ready to watch events"}`)
75-
f.Flush()
83+
heartbeat := time.NewTicker(defaultHeartbeatInterval)
84+
defer heartbeat.Stop()
7685

7786
watchChan := r.cli.Watch(ctx, store.RootKey, clientv3.WithPrefix())
87+
7888
for {
7989
select {
8090
case <-ctx.Done():
8191
log.Info().Msg("SSE client disconnected")
8292
return
8393

94+
case <-heartbeat.C:
95+
select {
96+
case eventCh <- ": ping\n\n":
97+
default:
98+
log.Debug().Msg("Heartbeat skipped, client lento")
99+
}
100+
84101
case watchResp, ok := <-watchChan:
85102
if !ok {
86103
log.Warn().Msg("Etcd watch channel closed")
87104
return
88105
}
106+
if err := watchResp.Err(); err != nil {
107+
log.Error().Err(err).Msg("Error from ETCD watch")
108+
continue
109+
}
89110

90111
for _, ev := range watchResp.Events {
91-
key := string(ev.Kv.Key)
92112
val := ev.Kv.Value
113+
key := string(ev.Kv.Key)
93114
if len(val) == 0 {
94115
continue
95116
}
@@ -101,48 +122,39 @@ func (r *handler) ServeHTTP(wri http.ResponseWriter, req *http.Request) {
101122
}
102123

103124
cid := labels.CompositionID(&obj)
104-
belongsToComposition := len(cid) > 0
105-
106125
eventName := "krateo"
107-
if len(cid) > 0 {
126+
if cid != "" {
108127
eventName = cid
109128
}
110129

111-
zle := log.Debug().
130+
log.Debug().
112131
Str("id", key).
132+
Str("event", eventName).
113133
Str("reason", obj.Reason).
114134
Str("message", obj.Message).
115135
Str("involvedObject.Name", obj.InvolvedObject.Name).
116-
Str("involvedObject.Namespace", obj.InvolvedObject.Namespace)
117-
118-
if belongsToComposition {
119-
zle.Str("event", cid)
120-
} else {
121-
zle.Str("event", "krateo")
122-
}
123-
zle.Msg("Sending SSE")
124-
zle = nil
136+
Str("involvedObject.Namespace", obj.InvolvedObject.Namespace).
137+
Msg("Queueing SSE event")
125138

126-
fmt.Fprintf(wri, "event: %s\n", eventName)
127-
fmt.Fprintf(wri, "id: %s\n", key)
128-
fmt.Fprintf(wri, "data: %s\n\n", string(val))
129-
f.Flush()
139+
payload := fmt.Sprintf("event: %s\nid: %s\ndata: %s\n\n", eventName, key, string(val))
130140

131-
log.Debug().
132-
Str("event", eventName).
133-
Str("key", key).
134-
Msg("SSE sent")
141+
// invio non bloccante
142+
select {
143+
case eventCh <- payload:
144+
default:
145+
log.Warn().Str("event", eventName).Str("key", key).Msg("Dropping SSE event, client troppo lento")
146+
}
135147
}
136148
}
137149
}
138150
}
139151

140-
// setCORSHeaders aggiunge header CORS generali
141152
func (r *handler) setCORSHeaders(w http.ResponseWriter) {
142153
w.Header().Set("Access-Control-Allow-Origin", "*")
143-
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
144-
w.Header().Set("Access-Control-Allow-Headers", "Accept, Authorization, Content-Type, X-Auth-Code, X-Krateo-TraceId")
145-
w.Header().Set("Access-Control-Expose-Headers", "Link,Authorization,Content-Type")
146-
w.Header().Set("Access-Control-Allow-Headers", "Authorization,Content-Type")
154+
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
155+
w.Header().Set("Access-Control-Allow-Headers",
156+
"Accept, Authorization, Content-Type, X-Auth-Code, X-Krateo-TraceId")
157+
w.Header().Set("Access-Control-Expose-Headers",
158+
"Link, Authorization, Content-Type")
147159
w.Header().Set("Access-Control-Allow-Credentials", "true")
148160
}

eventsse/main.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ import (
2828
)
2929

3030
const (
31-
serviceName = "eventsse"
32-
defaultLimit = 100
33-
fifoMultiplier = 10
31+
serviceName = "eventsse"
32+
defaultLimit = 100
3433
)
3534

3635
func main() {

0 commit comments

Comments
 (0)