fix: address code review findings
- Start Web UI before the blocking initial sync so the dashboard is reachable during the (rate-limited, potentially multi-minute) first sync; fold the immediate sync into startPeriodicSync's overlap guard so it can never race a concurrent tick over the shared DB / MB client. - Make MarkNotificationSent idempotent: INSERT OR IGNORE for same-second PK collisions, and explicitly swallow FK violations when a release was pruned by a concurrent re-sync. Prevents a single vanished/duplicate release from aborting the digest mark-sent loop and re-sending. - Do not abort NotifyOnce's mark-sent loop on a single failure; log and continue so every release in the batch is marked. - NULL-safe reads: COALESCE(type,''), COALESCE(release_date,'') in the external_releases and unnotified readers to match the cache reader. - Update/extend tests for the new idempotency and startup contracts.
This commit is contained in:
@@ -131,17 +131,14 @@ func (a *App) Close() {
|
||||
}
|
||||
|
||||
func (a *App) run(ctx context.Context) error {
|
||||
// Run an immediate sync+scan so the service produces results without
|
||||
// waiting a full interval.
|
||||
if err := a.doSync(ctx); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
log.Printf("Initial sync+scan failed: %v", err)
|
||||
}
|
||||
|
||||
// Start the Web UI dashboard in its own goroutine; it serves until ctx is
|
||||
// cancelled, then shuts down gracefully.
|
||||
// Start the Web UI dashboard FIRST, in its own goroutine, so the dashboard
|
||||
// accepts connections immediately. The initial sync below is throttled by
|
||||
// the MusicBrainz 1 req/s limit and can take many minutes on a large
|
||||
// library (worst case: a fresh DB where every artist needs MBID
|
||||
// resolution) — exactly when an operator is most likely watching. Starting
|
||||
// the server first means the dashboard is reachable (serving cached data)
|
||||
// during that window instead of refusing connections. It serves until ctx
|
||||
// is cancelled, then shuts down gracefully.
|
||||
if a.web != nil {
|
||||
go func() {
|
||||
if err := a.web.Start(ctx); err != nil {
|
||||
@@ -157,7 +154,10 @@ func (a *App) run(ctx context.Context) error {
|
||||
// disabled (sender nil / enabled false), so always calling it is safe.
|
||||
a.startNotifier(ctx)
|
||||
|
||||
// Kick off the periodic sync+scan loop goroutine.
|
||||
// Kick off the periodic sync+scan loop. It runs an immediate first sync
|
||||
// (governed by the same overlap guard as periodic ticks) so the service
|
||||
// produces results without waiting a full interval, without racing a
|
||||
// concurrent tick over the shared DB and rate-limited MusicBrainz client.
|
||||
a.startPeriodicSync(ctx)
|
||||
|
||||
<-ctx.Done()
|
||||
@@ -237,25 +237,38 @@ func (a *App) startPeriodicSync(ctx context.Context) {
|
||||
var free = make(chan struct{}, 1)
|
||||
free <- struct{}{}
|
||||
|
||||
// launch starts a guarded sync if the slot is free, returning true when a
|
||||
// sync was started and false when one is already in progress. The in-flight
|
||||
// goroutine returns the token when done.
|
||||
launch := func(label string) bool {
|
||||
select {
|
||||
case <-free:
|
||||
go func() {
|
||||
defer func() { free <- struct{}{} }()
|
||||
if err := a.doSync(ctx); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
log.Printf("%s sync+scan failed: %v", label, err)
|
||||
}
|
||||
}()
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Immediate first sync (guarded), so the service produces results without
|
||||
// waiting a full interval and without racing the first ticker fire.
|
||||
launch("Initial")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("Periodic sync stopped.")
|
||||
return
|
||||
case <-ticker.C:
|
||||
select {
|
||||
case <-free:
|
||||
// Slot was free; start a sync and release the slot when done.
|
||||
go func() {
|
||||
defer func() { free <- struct{}{} }()
|
||||
if err := a.doSync(ctx); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
log.Printf("Periodic sync+scan failed: %v", err)
|
||||
}
|
||||
}()
|
||||
default:
|
||||
if !launch("Periodic") {
|
||||
// Previous sync still running; skip this tick.
|
||||
log.Println("Skipping periodic sync: previous sync still in progress.")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user