@@ -18,14 +18,14 @@ import (
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
cfg *config.Config
|
||||
store *db.Store
|
||||
client *http.Client
|
||||
stop chan struct{}
|
||||
once sync.Once
|
||||
mu sync.RWMutex
|
||||
jobs map[string]CheckJob
|
||||
events chan Event
|
||||
cfg *config.Config
|
||||
store *db.Store
|
||||
client *http.Client
|
||||
stop chan struct{}
|
||||
once sync.Once
|
||||
mu sync.RWMutex
|
||||
jobs map[string]CheckJob
|
||||
subscribers map[chan Event]struct{}
|
||||
}
|
||||
|
||||
type Event struct {
|
||||
@@ -68,12 +68,12 @@ type legacySubcategory struct {
|
||||
|
||||
func NewService(cfg *config.Config, store *db.Store) *Service {
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
store: store,
|
||||
client: &http.Client{Timeout: 10 * time.Second},
|
||||
stop: make(chan struct{}),
|
||||
jobs: map[string]CheckJob{},
|
||||
events: make(chan Event, 32),
|
||||
cfg: cfg,
|
||||
store: store,
|
||||
client: &http.Client{Timeout: 10 * time.Second},
|
||||
stop: make(chan struct{}),
|
||||
jobs: map[string]CheckJob{},
|
||||
subscribers: map[chan Event]struct{}{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,8 +296,20 @@ func (s *Service) CheckJob(id string) (CheckJob, bool) {
|
||||
return item, ok
|
||||
}
|
||||
|
||||
func (s *Service) Events() <-chan Event {
|
||||
return s.events
|
||||
func (s *Service) SubscribeEvents() (<-chan Event, func()) {
|
||||
ch := make(chan Event, 16)
|
||||
s.mu.Lock()
|
||||
s.subscribers[ch] = struct{}{}
|
||||
s.mu.Unlock()
|
||||
unsubscribe := func() {
|
||||
s.mu.Lock()
|
||||
if _, ok := s.subscribers[ch]; ok {
|
||||
delete(s.subscribers, ch)
|
||||
close(ch)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
return ch, unsubscribe
|
||||
}
|
||||
|
||||
func (s *Service) runCheckJob(ctx context.Context, id string, items []db.Source) {
|
||||
@@ -445,9 +457,13 @@ func (s *Service) updateJob(id string, mutate func(*CheckJob)) {
|
||||
|
||||
func (s *Service) emit(kind string, data map[string]any) {
|
||||
event := Event{Type: kind, Data: data}
|
||||
select {
|
||||
case s.events <- event:
|
||||
default:
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
for ch := range s.subscribers {
|
||||
select {
|
||||
case ch <- event:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"ymhut-box/server/unified-management/internal/config"
|
||||
"ymhut-box/server/unified-management/internal/db"
|
||||
@@ -57,6 +58,67 @@ func TestCheckOneTreatsRedirectToOKAsRedirected(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestQueueCheckAllUsesBackgroundContext(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
defer server.Close()
|
||||
cfg, store := testStore(t)
|
||||
service := NewService(cfg, store)
|
||||
if _, err := store.UpsertSource(db.Source{
|
||||
CategoryID: "test",
|
||||
CategoryName: "Test",
|
||||
SourceID: "slow-ok",
|
||||
Name: "Slow OK",
|
||||
Method: "GET",
|
||||
APIURL: server.URL,
|
||||
TimeoutMS: 1000,
|
||||
CheckIntervalSec: 300,
|
||||
Enabled: true,
|
||||
ClientVisible: true,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
job := service.QueueCheckAll()
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
current, ok := service.CheckJob(job.ID)
|
||||
if ok && current.Status == "completed" {
|
||||
if current.Stats["ok"] != 1 {
|
||||
t.Fatalf("stats = %#v, want one ok", current.Stats)
|
||||
}
|
||||
return
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("job did not complete: %#v", job)
|
||||
}
|
||||
|
||||
func TestSubscribeEventsBroadcastsToAllSubscribers(t *testing.T) {
|
||||
cfg, store := testStore(t)
|
||||
service := NewService(cfg, store)
|
||||
eventsA, unsubscribeA := service.SubscribeEvents()
|
||||
defer unsubscribeA()
|
||||
eventsB, unsubscribeB := service.SubscribeEvents()
|
||||
defer unsubscribeB()
|
||||
|
||||
service.emit("source_check.completed", map[string]any{"jobId": "demo"})
|
||||
|
||||
assertEvent := func(name string, events <-chan Event) {
|
||||
t.Helper()
|
||||
select {
|
||||
case event := <-events:
|
||||
if event.Type != "source_check.completed" || event.Data["jobId"] != "demo" {
|
||||
t.Fatalf("%s received unexpected event: %#v", name, event)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatalf("%s did not receive broadcast event", name)
|
||||
}
|
||||
}
|
||||
assertEvent("subscriber A", eventsA)
|
||||
assertEvent("subscriber B", eventsB)
|
||||
}
|
||||
|
||||
func testStore(t *testing.T) (*config.Config, *db.Store) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
|
||||
Reference in New Issue
Block a user