bfa006f4ab
- SSH key management: generate ed25519 keypairs or import public keys from UI (/ssh-keys), per-machine key selection in Machines form, one-time private key download with hash verification - Fix engine to use machine-specific SSH key (was hardcoded to server key) - Job log persistence: write to job_logs table (DB) with batched inserts, buffer of 50 lines; GetAllFiltered with status/pair/date range filters - EventBus refactor: per-job subscriber channels, global channel, non-blocking - SSE endpoints: /jobs/stream (all), /jobs/:id/log/stream (per-job live) - JobDetail page: live log streaming, auto-scroll, cancel, duration - JobHistory: filters (pair, status, date range), pagination, link to detail - Cleanup scheduler: daily purge of job_logs and finished jobs older than SYNCSERVER_RETENTION_DAYS (default 30) - Migration 0002: indexes on job_logs(job_id), jobs(status,created_at), jobs(sync_pair_id)
93 lines
1.9 KiB
Go
93 lines
1.9 KiB
Go
package syncengine
|
|
|
|
import (
|
|
"log/slog"
|
|
"sync"
|
|
)
|
|
|
|
type EventBus struct {
|
|
subscribers map[int64]map[chan Event]struct{}
|
|
mu sync.RWMutex
|
|
global chan Event
|
|
bufferSize int
|
|
}
|
|
|
|
func NewEventBus(bufferSize int) *EventBus {
|
|
return &EventBus{
|
|
subscribers: make(map[int64]map[chan Event]struct{}),
|
|
global: make(chan Event, bufferSize),
|
|
bufferSize: bufferSize,
|
|
}
|
|
}
|
|
|
|
func (eb *EventBus) Subscribe(jobID int64) (chan Event, func()) {
|
|
eb.mu.Lock()
|
|
defer eb.mu.Unlock()
|
|
if eb.subscribers[jobID] == nil {
|
|
eb.subscribers[jobID] = make(map[chan Event]struct{})
|
|
}
|
|
ch := make(chan Event, eb.bufferSize)
|
|
eb.subscribers[jobID][ch] = struct{}{}
|
|
unsubscribe := func() {
|
|
eb.mu.Lock()
|
|
defer eb.mu.Unlock()
|
|
if subs, ok := eb.subscribers[jobID]; ok {
|
|
delete(subs, ch)
|
|
if len(subs) == 0 {
|
|
delete(eb.subscribers, jobID)
|
|
}
|
|
}
|
|
close(ch)
|
|
}
|
|
return ch, unsubscribe
|
|
}
|
|
|
|
func (eb *EventBus) SubscribeGlobal() (chan Event, func()) {
|
|
eb.mu.RLock()
|
|
ch := make(chan Event, eb.bufferSize)
|
|
eb.mu.RUnlock()
|
|
go func() {
|
|
for evt := range eb.global {
|
|
select {
|
|
case ch <- evt:
|
|
default:
|
|
slog.Warn("global event subscriber buffer full, dropping event", "type", evt.Type)
|
|
}
|
|
}
|
|
close(ch)
|
|
}()
|
|
return ch, func() { close(ch) }
|
|
}
|
|
|
|
func (eb *EventBus) Publish(evt Event) {
|
|
eb.mu.RLock()
|
|
defer eb.mu.RUnlock()
|
|
|
|
if subs, ok := eb.subscribers[evt.JobID]; ok {
|
|
for ch := range subs {
|
|
select {
|
|
case ch <- evt:
|
|
default:
|
|
slog.Warn("job event subscriber buffer full, dropping event", "job_id", evt.JobID)
|
|
}
|
|
}
|
|
}
|
|
|
|
select {
|
|
case eb.global <- evt:
|
|
default:
|
|
slog.Warn("global event bus full, dropping event", "type", evt.Type)
|
|
}
|
|
}
|
|
|
|
func (eb *EventBus) CloseJobChannels(jobID int64) {
|
|
eb.mu.Lock()
|
|
defer eb.mu.Unlock()
|
|
if subs, ok := eb.subscribers[jobID]; ok {
|
|
for ch := range subs {
|
|
close(ch)
|
|
}
|
|
delete(eb.subscribers, jobID)
|
|
}
|
|
}
|