converge-relay: phase 1 — webhook-to-SSE relay server in Go
- Pub/sub broker with fan-out to SSE clients - POST /webhook — receives Gitea push events, optional HMAC verification - GET /events — SSE stream of push events - GET /health — health check endpoint - Zero external dependencies (stdlib only) - Docker-based build via goctl wrapper (golang:1.26-alpine) - systemd unit, README with deployment docs - Tests: broker pub/sub, slow subscriber drop, ref parsing, signature verification - 6.0M static binary
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import "log/slog"
|
||||
|
||||
// PushEvent is broadcast to all SSE subscribers when a webhook is received.
|
||||
// It contains just enough information for converge clients to decide whether
|
||||
// to pull.
|
||||
type PushEvent struct {
|
||||
FullName string `json:"full_name"` // e.g. "myorg/myrepo"
|
||||
Branch string `json:"branch"` // e.g. "main"
|
||||
Ref string `json:"ref"` // e.g. "refs/heads/main"
|
||||
}
|
||||
|
||||
// Broker implements a pub/sub fan-out for PushEvents.
|
||||
// Subscribe to get a channel that receives events; unsubscribe when done.
|
||||
// Slow subscribers (channel full) are silently dropped for that event.
|
||||
type Broker struct {
|
||||
subscribe chan chan PushEvent
|
||||
unsubscribe chan chan PushEvent
|
||||
publish chan PushEvent
|
||||
stop chan struct{}
|
||||
subs map[chan PushEvent]struct{}
|
||||
}
|
||||
|
||||
// NewBroker creates a ready-to-run Broker.
|
||||
func NewBroker() *Broker {
|
||||
return &Broker{
|
||||
subscribe: make(chan chan PushEvent),
|
||||
unsubscribe: make(chan chan PushEvent),
|
||||
publish: make(chan PushEvent, 64),
|
||||
stop: make(chan struct{}),
|
||||
subs: make(map[chan PushEvent]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Run starts the broker event loop. Must be called in its own goroutine.
|
||||
func (b *Broker) Run() {
|
||||
for {
|
||||
select {
|
||||
case ch := <-b.subscribe:
|
||||
b.subs[ch] = struct{}{}
|
||||
slog.Debug("subscriber connected", "total", len(b.subs))
|
||||
|
||||
case ch := <-b.unsubscribe:
|
||||
delete(b.subs, ch)
|
||||
// Drain and close the channel so readers exit cleanly
|
||||
close(ch)
|
||||
for range ch {
|
||||
}
|
||||
slog.Debug("subscriber disconnected", "total", len(b.subs))
|
||||
|
||||
case evt := <-b.publish:
|
||||
slog.Info("publishing event", "full_name", evt.FullName, "branch", evt.Branch)
|
||||
for ch := range b.subs {
|
||||
select {
|
||||
case ch <- evt:
|
||||
default:
|
||||
// Subscriber too slow; drop the event for this subscriber
|
||||
slog.Warn("dropping event for slow subscriber")
|
||||
}
|
||||
}
|
||||
|
||||
case <-b.stop:
|
||||
for ch := range b.subs {
|
||||
close(ch)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe returns a buffered channel that receives PushEvents.
|
||||
func (b *Broker) Subscribe() chan PushEvent {
|
||||
// Small buffer to smooth out delivery; slow consumers are dropped by
|
||||
// the broker's non-blocking publish anyway.
|
||||
ch := make(chan PushEvent, 16)
|
||||
b.subscribe <- ch
|
||||
return ch
|
||||
}
|
||||
|
||||
// Unsubscribe removes a subscriber channel.
|
||||
func (b *Broker) Unsubscribe(ch chan PushEvent) {
|
||||
b.unsubscribe <- ch
|
||||
}
|
||||
|
||||
// Publish sends an event to all current subscribers. Never blocks.
|
||||
func (b *Broker) Publish(evt PushEvent) {
|
||||
b.publish <- evt
|
||||
}
|
||||
|
||||
// Stop shuts down the broker.
|
||||
func (b *Broker) Stop() {
|
||||
close(b.stop)
|
||||
}
|
||||
Reference in New Issue
Block a user