diff --git a/app.go b/app.go index bd5d564..2c5947f 100644 --- a/app.go +++ b/app.go @@ -9455,6 +9455,15 @@ func (a *App) LogUDPLoggedADIF(adifText string) (int64, error) { if a.qso == nil { return 0, fmt.Errorf("db not initialized") } + // Serialise the WHOLE operation — parse, callsign LOOKUP, dedup and insert — + // so simultaneous UDP QSOs are processed one at a time (a queue). A multi-stream + // MSHV finishing 4 QSOs at once used to fire 4 concurrent callsign lookups and DB + // writes, which the QRZ/HamQTH service (and a remote DB) rejected with "too many + // requests". Holding the lock across the lookup spaces them out; the UDP reader + // runs on its own goroutine, so this never blocks packet reception — callers just + // queue behind each other. (adifwatch shares this lock for the same reason.) + a.udpLogMu.Lock() + defer a.udpLogMu.Unlock() // Pull the first record out of the payload. WSJT-X / JTDX / MSHV // always send a single QSO per UDP packet (no header) but we tolerate // either form via adif.Parse. @@ -9597,12 +9606,10 @@ func (a *App) LogUDPLoggedADIF(adifText string) (int64, error) { // a minute (the two apps stamp their own time), so a minute-exact key // missed it and the contact got duplicated. // - // The check + insert is guarded by udpLogMu: MSHV/WSJT can deliver the same - // logged-QSO packet twice in quick succession (re-broadcast, or two - // listeners), and without serialisation both goroutines read the dedup set - // BEFORE either inserts, both pass, and the QSO lands twice. - a.udpLogMu.Lock() - defer a.udpLogMu.Unlock() + // The check + insert is covered by udpLogMu (taken at the top of this function): + // MSHV/WSJT can deliver the same logged-QSO packet twice in quick succession + // (re-broadcast, or two listeners), and without serialisation both goroutines + // read the dedup set BEFORE either inserts, both pass, and the QSO lands twice. seen, err := a.qso.ExistingDedupeKeys(a.ctx) if err == nil { base := q.QSODate.UTC()