fix(delete): stop asking Club Log to delete QSOs it never had
Deleting a selection with 'delete from the remote services' on took
minutes. Club Log was called for EVERY deleted QSO, uploaded or not, and
for the ones it never had it answers 403 -- a full network round trip per
contact, in series, on the goroutine the UI was waiting on. QRZ already
skipped records with no stored logid; Club Log had no equivalent guard.
Three changes, each fixing one part of the delay:
- only contacts whose clublog upload status is Y or M are withdrawn;
- the withdrawals run in the background, after a snapshot -- the local
log is the operator's own copy and must not wait on two websites;
- the hooks read the rows in one query instead of one per id, which on
a remote MySQL logbook was 2 round trips per QSO before the DELETE.
Consecutive refusals now abort the run: Club Log blocks an account that
keeps sending requests it refuses, so working through the rest of the
selection only earns a longer block.
This commit is contained in:
@@ -6451,6 +6451,26 @@ func (a *App) DeleteQSO(id int64) error {
|
||||
// Failures never block the local delete. The operator asked for this QSO to go;
|
||||
// a refusing website is not a reason to keep it, and the log says what happened
|
||||
// so it can be sorted out on the site afterwards.
|
||||
// deleteSnapshot reads the rows behind a set of ids in ONE query.
|
||||
//
|
||||
// The delete hooks used to fetch them one by one. That is a round trip per
|
||||
// contact, and on a remote MySQL logbook 200 deletions became 400 sequential
|
||||
// queries before the DELETE was even issued — the reason deleting a selection
|
||||
// felt broken rather than slow.
|
||||
func (a *App) deleteSnapshot(ids []int64) []qso.QSO {
|
||||
if a.qso == nil || len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]qso.QSO, 0, len(ids))
|
||||
if err := a.qso.IterateByIDs(a.ctx, ids, func(q qso.QSO) error {
|
||||
out = append(out, q)
|
||||
return nil
|
||||
}); err != nil {
|
||||
applog.Printf("delete: reading %d QSO(s) before deletion failed: %v", len(ids), err)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (a *App) deleteRemoteCopies(ids []int64) {
|
||||
if a.qso == nil || len(ids) == 0 {
|
||||
return
|
||||
@@ -6469,9 +6489,37 @@ func (a *App) deleteRemoteCopies(ids []int64) {
|
||||
if !doQRZ && !doClublog {
|
||||
return
|
||||
}
|
||||
for _, id := range ids {
|
||||
q, err := a.qso.GetByID(a.ctx, id)
|
||||
if err != nil || q.Callsign == "" {
|
||||
// Snapshot first — the rows are about to disappear — then withdraw them in
|
||||
// the background. Each service is an HTTP round trip PER CONTACT, so doing
|
||||
// this on the caller's goroutine held the whole UI for as long as the
|
||||
// websites took: minutes for a large selection, with nothing on screen
|
||||
// saying why. The local log is the operator's own copy and must not wait on
|
||||
// two remote sites to answer.
|
||||
rows := a.deleteSnapshot(ids)
|
||||
go a.withdrawRemoteCopies(rows, cfg, doQRZ, doClublog)
|
||||
}
|
||||
|
||||
// maxClublogRefusals is how many consecutive refusals are tolerated before the
|
||||
// withdrawals stop. Three is enough to tell a one-off from a blocked account.
|
||||
const maxClublogRefusals = 3
|
||||
|
||||
// clublogWasUploaded reports whether this QSO ever reached Club Log. "M"
|
||||
// (modified since upload) counts: the copy is there, it is merely out of date.
|
||||
func clublogWasUploaded(status string) bool {
|
||||
switch strings.ToUpper(strings.TrimSpace(status)) {
|
||||
case "Y", "M":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *App) withdrawRemoteCopies(rows []qso.QSO, cfg extsvc.ExternalServices, doQRZ, doClublog bool) {
|
||||
started := time.Now()
|
||||
clublogRefused := 0
|
||||
for i := range rows {
|
||||
q := rows[i]
|
||||
id := q.ID
|
||||
if q.Callsign == "" {
|
||||
continue
|
||||
}
|
||||
if doQRZ {
|
||||
@@ -6490,14 +6538,31 @@ func (a *App) deleteRemoteCopies(ids []int64) {
|
||||
applog.Printf("extsvc: QRZ delete of QSO %d (%s): %s", id, q.Callsign, msg)
|
||||
}
|
||||
}
|
||||
if doClublog {
|
||||
// Only ask Club Log about contacts it was actually given. A QSO that was
|
||||
// never uploaded has no copy to withdraw, and asking anyway is not merely
|
||||
// pointless: Club Log answers 403 and starts blocking the account, so a
|
||||
// selection of never-uploaded QSOs turned into hundreds of refused
|
||||
// requests, one after another, each waiting on the network.
|
||||
if doClublog && !clublogWasUploaded(q.ClublogUploadStatus) {
|
||||
applog.Printf("extsvc: QSO %d (%s) was never uploaded to Club Log — nothing to withdraw", id, q.Callsign)
|
||||
} else if doClublog {
|
||||
if msg, err := extsvc.DeleteClublog(a.ctx, nil, cfg.Clublog, q.Callsign, q.QSODate, q.Band); err != nil {
|
||||
clublogRefused++
|
||||
applog.Printf("extsvc: Club Log delete of QSO %d (%s) failed: %v", id, q.Callsign, err)
|
||||
// Club Log blocks an account that keeps sending requests it
|
||||
// refuses. Stop after a few in a row rather than work through
|
||||
// the whole selection earning a longer block.
|
||||
if clublogRefused >= maxClublogRefusals {
|
||||
applog.Printf("extsvc: Club Log refused %d deletions in a row — giving up on the rest", clublogRefused)
|
||||
doClublog = false
|
||||
}
|
||||
} else {
|
||||
clublogRefused = 0
|
||||
applog.Printf("extsvc: Club Log delete of QSO %d (%s): %s", id, q.Callsign, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
applog.Printf("extsvc: withdrew %d deleted QSO(s) from the remote services in %s", len(rows), time.Since(started).Round(time.Millisecond))
|
||||
}
|
||||
|
||||
// DeleteQSOs removes several QSOs at once (multi-row selection). Returns the
|
||||
@@ -6506,16 +6571,21 @@ func (a *App) DeleteQSOs(ids []int64) (int64, error) {
|
||||
if a.qso == nil {
|
||||
return 0, fmt.Errorf("db not initialized")
|
||||
}
|
||||
started := time.Now()
|
||||
a.deleteRemoteCopies(ids)
|
||||
a.syncPublishDeletes(ids)
|
||||
beforeDelete := time.Since(started)
|
||||
n, err := a.qso.DeleteMany(a.ctx, ids)
|
||||
// Logged because a delete that does nothing is otherwise indistinguishable
|
||||
// from a delete that worked: the rows leave the grid either way once it
|
||||
// reloads, and the only place the truth survives is here.
|
||||
if err != nil {
|
||||
applog.Printf("delete: %d QSO(s) requested, FAILED: %v", len(ids), err)
|
||||
applog.Printf("delete: %d QSO(s) requested, FAILED after %s: %v", len(ids), time.Since(started).Round(time.Millisecond), err)
|
||||
} else {
|
||||
applog.Printf("delete: %d QSO(s) requested, %d row(s) removed", len(ids), n)
|
||||
// Both timings, because they blame different things: a long prelude is
|
||||
// the sync/withdraw hooks, a long DELETE is the database itself.
|
||||
applog.Printf("delete: %d QSO(s) requested, %d row(s) removed — %s before the delete, %s total",
|
||||
len(ids), n, beforeDelete.Round(time.Millisecond), time.Since(started).Round(time.Millisecond))
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user