224 lines
4.8 KiB
Go
224 lines
4.8 KiB
Go
package watcher
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"log"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
// MaxRetries is the maximum number of delivery retries
|
|
MaxRetries = 3
|
|
// RetryDelay is the delay between retries
|
|
RetryDelay = 5 * time.Second
|
|
// WebhookTimeout is the timeout for webhook requests
|
|
WebhookTimeout = 10 * time.Second
|
|
// MaxFailCount is the max failures before removing subscriber
|
|
MaxFailCount = 10
|
|
)
|
|
|
|
// Dispatcher sends file events to subscribers
|
|
type Dispatcher struct {
|
|
subscribers map[string]*Subscriber
|
|
sseClients map[chan FileEvent]bool
|
|
mu sync.RWMutex
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewDispatcher creates a new event dispatcher
|
|
func NewDispatcher() *Dispatcher {
|
|
return &Dispatcher{
|
|
subscribers: make(map[string]*Subscriber),
|
|
sseClients: make(map[chan FileEvent]bool),
|
|
httpClient: &http.Client{
|
|
Timeout: WebhookTimeout,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Subscribe registers a webhook subscriber
|
|
func (d *Dispatcher) Subscribe(url string, eventTypes []string) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
d.subscribers[url] = &Subscriber{
|
|
URL: url,
|
|
RegisteredAt: NowMS(),
|
|
EventTypes: eventTypes,
|
|
FailCount: 0,
|
|
}
|
|
|
|
log.Printf("[Dispatcher] Subscribed webhook: %s", url)
|
|
return nil
|
|
}
|
|
|
|
// Unsubscribe removes a webhook subscriber
|
|
func (d *Dispatcher) Unsubscribe(url string) error {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
delete(d.subscribers, url)
|
|
log.Printf("[Dispatcher] Unsubscribed webhook: %s", url)
|
|
return nil
|
|
}
|
|
|
|
// ListSubscribers returns all subscribers
|
|
func (d *Dispatcher) ListSubscribers() []Subscriber {
|
|
d.mu.RLock()
|
|
defer d.mu.RUnlock()
|
|
|
|
result := make([]Subscriber, 0, len(d.subscribers))
|
|
for _, sub := range d.subscribers {
|
|
result = append(result, *sub)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// AddSSEClient adds an SSE client channel
|
|
func (d *Dispatcher) AddSSEClient(ch chan FileEvent) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
d.sseClients[ch] = true
|
|
}
|
|
|
|
// RemoveSSEClient removes an SSE client channel
|
|
func (d *Dispatcher) RemoveSSEClient(ch chan FileEvent) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
delete(d.sseClients, ch)
|
|
close(ch)
|
|
}
|
|
|
|
// Dispatch sends an event to all subscribers
|
|
func (d *Dispatcher) Dispatch(event FileEvent) {
|
|
// Dispatch to webhooks
|
|
go d.dispatchToWebhooks(event)
|
|
|
|
// Dispatch to SSE clients
|
|
d.dispatchToSSE(event)
|
|
}
|
|
|
|
// dispatchToWebhooks sends event to webhook subscribers
|
|
func (d *Dispatcher) dispatchToWebhooks(event FileEvent) {
|
|
d.mu.RLock()
|
|
subscribers := make([]*Subscriber, 0, len(d.subscribers))
|
|
for _, sub := range d.subscribers {
|
|
subscribers = append(subscribers, sub)
|
|
}
|
|
d.mu.RUnlock()
|
|
|
|
for _, sub := range subscribers {
|
|
// Check event type filter
|
|
if len(sub.EventTypes) > 0 {
|
|
found := false
|
|
for _, et := range sub.EventTypes {
|
|
if et == string(event.Type) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
continue
|
|
}
|
|
}
|
|
|
|
go d.deliverToWebhook(sub.URL, event)
|
|
}
|
|
}
|
|
|
|
// deliverToWebhook delivers an event to a webhook with retries
|
|
func (d *Dispatcher) deliverToWebhook(url string, event FileEvent) {
|
|
body, err := json.Marshal(event)
|
|
if err != nil {
|
|
log.Printf("[Dispatcher] Failed to marshal event: %v", err)
|
|
return
|
|
}
|
|
|
|
var lastErr error
|
|
for attempt := 0; attempt < MaxRetries; attempt++ {
|
|
if attempt > 0 {
|
|
time.Sleep(RetryDelay)
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("X-Event-Type", string(event.Type))
|
|
|
|
resp, err := d.httpClient.Do(req)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
|
// Success
|
|
d.mu.Lock()
|
|
if sub, ok := d.subscribers[url]; ok {
|
|
sub.LastDelivery = NowMS()
|
|
sub.FailCount = 0
|
|
}
|
|
d.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
lastErr = &httpError{StatusCode: resp.StatusCode}
|
|
}
|
|
|
|
// All retries failed
|
|
log.Printf("[Dispatcher] Failed to deliver to %s after %d retries: %v", url, MaxRetries, lastErr)
|
|
|
|
d.mu.Lock()
|
|
if sub, ok := d.subscribers[url]; ok {
|
|
sub.FailCount++
|
|
if sub.FailCount >= MaxFailCount {
|
|
delete(d.subscribers, url)
|
|
log.Printf("[Dispatcher] Removed webhook %s after %d failures", url, MaxFailCount)
|
|
}
|
|
}
|
|
d.mu.Unlock()
|
|
}
|
|
|
|
// dispatchToSSE sends event to SSE clients
|
|
func (d *Dispatcher) dispatchToSSE(event FileEvent) {
|
|
d.mu.RLock()
|
|
clients := make([]chan FileEvent, 0, len(d.sseClients))
|
|
for ch := range d.sseClients {
|
|
clients = append(clients, ch)
|
|
}
|
|
d.mu.RUnlock()
|
|
|
|
for _, ch := range clients {
|
|
select {
|
|
case ch <- event:
|
|
default:
|
|
// Channel full, skip
|
|
}
|
|
}
|
|
}
|
|
|
|
// SSEClientCount returns the number of connected SSE clients
|
|
func (d *Dispatcher) SSEClientCount() int {
|
|
d.mu.RLock()
|
|
defer d.mu.RUnlock()
|
|
return len(d.sseClients)
|
|
}
|
|
|
|
// httpError represents an HTTP error
|
|
type httpError struct {
|
|
StatusCode int
|
|
}
|
|
|
|
func (e *httpError) Error() string {
|
|
return http.StatusText(e.StatusCode)
|
|
}
|