fix: UDP-logged QSOs fall back to the offline outbox on DB failure (were silently lost); speed up MySQL connect (batch migration checks into 1 query, connect direct-to-DB first) — was ~40s on a high-latency link
This commit is contained in:
@@ -9623,6 +9623,14 @@ func (a *App) LogUDPLoggedADIF(adifText string) (int64, error) {
|
|||||||
|
|
||||||
id, err := a.qso.Add(a.ctx, q)
|
id, err := a.qso.Add(a.ctx, q)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// DB UNREACHABLE (drop / timeout on a slow-or-broken MySQL) — park the QSO
|
||||||
|
// in the offline outbox rather than lose it, exactly like the manual log
|
||||||
|
// path. Without this, a laggy shared MySQL silently dropped UDP-logged QSOs
|
||||||
|
// from a multi-stream MSHV. Returns -1 so the caller treats it as "saved,
|
||||||
|
// waiting to sync", not a failure.
|
||||||
|
if db.IsConnLost(err) && a.queueOffline(q, err) {
|
||||||
|
return -1, nil
|
||||||
|
}
|
||||||
return 0, fmt.Errorf("insert qso: %w", err)
|
return 0, fmt.Errorf("insert qso: %w", err)
|
||||||
}
|
}
|
||||||
q.ID = id
|
q.ID = id
|
||||||
@@ -9633,6 +9641,10 @@ func (a *App) LogUDPLoggedADIF(adifText string) (int64, error) {
|
|||||||
a.extsvc.OnQSOLogged(id)
|
a.extsvc.OnQSOLogged(id)
|
||||||
}
|
}
|
||||||
a.maybeAutoSendEQSL(q)
|
a.maybeAutoSendEQSL(q)
|
||||||
|
// A successful write means the link is healthy again — flush anything parked.
|
||||||
|
if a.offlineMode && a.offlineQ != nil && a.offlineQ.Count() > 0 {
|
||||||
|
go func() { _, _ = a.replayOfflineQueue() }()
|
||||||
|
}
|
||||||
return id, nil
|
return id, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-7
@@ -198,14 +198,26 @@ func migrate(conn *sql.DB, translate func(string) string) error {
|
|||||||
}
|
}
|
||||||
sort.Strings(names)
|
sort.Strings(names)
|
||||||
|
|
||||||
for _, name := range names {
|
// Fetch every applied migration name in ONE query rather than one round-trip
|
||||||
var dummy string
|
// per migration. On a high-latency remote MySQL those 20+ SELECTs dominated the
|
||||||
err := conn.QueryRow(`SELECT name FROM schema_migrations WHERE name = ?`, name).Scan(&dummy)
|
// connect time (each RTT × migration count) — the whole logbook connect could
|
||||||
if err == nil {
|
// take tens of seconds. One SELECT + an in-memory set is effectively instant.
|
||||||
continue // already applied
|
applied := map[string]bool{}
|
||||||
|
if rows, err := conn.Query(`SELECT name FROM schema_migrations`); err == nil {
|
||||||
|
for rows.Next() {
|
||||||
|
var n string
|
||||||
|
if rows.Scan(&n) == nil {
|
||||||
|
applied[n] = true
|
||||||
}
|
}
|
||||||
if err != sql.ErrNoRows {
|
}
|
||||||
return fmt.Errorf("check migration %s: %w", name, err)
|
rows.Close()
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("read applied migrations: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
if applied[name] {
|
||||||
|
continue // already applied
|
||||||
}
|
}
|
||||||
content, err := migrationsFS.ReadFile("migrations/" + name)
|
content, err := migrationsFS.ReadFile("migrations/" + name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+19
-4
@@ -137,10 +137,6 @@ func OpenMySQL(c MySQLConfig) (*sql.DB, error) {
|
|||||||
if !validDBIdent(name) {
|
if !validDBIdent(name) {
|
||||||
return nil, fmt.Errorf("invalid database name %q (letters, digits, underscore only)", name)
|
return nil, fmt.Errorf("invalid database name %q (letters, digits, underscore only)", name)
|
||||||
}
|
}
|
||||||
// Ensure the database exists (connect server-level first).
|
|
||||||
if err := PingMySQL(c); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
conn, err := sql.Open("mysql", c.dsn())
|
conn, err := sql.Open("mysql", c.dsn())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("open mysql: %w", err)
|
return nil, fmt.Errorf("open mysql: %w", err)
|
||||||
@@ -157,10 +153,29 @@ func OpenMySQL(c MySQLConfig) (*sql.DB, error) {
|
|||||||
// (surfacing as "Unknown database"). Idle connections are still recycled
|
// (surfacing as "Unknown database"). Idle connections are still recycled
|
||||||
// after 90s, and the driver retries stale pooled connections.
|
// after 90s, and the driver retries stale pooled connections.
|
||||||
conn.SetConnMaxLifetime(0)
|
conn.SetConnMaxLifetime(0)
|
||||||
|
// Connect DIRECTLY to the database first. Only if that fails — the DB doesn't
|
||||||
|
// exist yet (first-time setup) or the server is unreachable — do we pay for the
|
||||||
|
// server-level connect + CREATE DATABASE (two more handshakes). On a normal
|
||||||
|
// startup against an existing DB this halves the connection round-trips, which
|
||||||
|
// matters a lot on a high-latency remote MySQL (each handshake is several RTTs).
|
||||||
|
if err := conn.Ping(); err != nil {
|
||||||
|
_ = conn.Close()
|
||||||
|
if perr := PingMySQL(c); perr != nil {
|
||||||
|
return nil, perr // creates the DB (or returns a clear "cannot create" error)
|
||||||
|
}
|
||||||
|
conn, err = sql.Open("mysql", c.dsn())
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open mysql: %w", err)
|
||||||
|
}
|
||||||
|
conn.SetMaxOpenConns(50)
|
||||||
|
conn.SetMaxIdleConns(10)
|
||||||
|
conn.SetConnMaxIdleTime(90 * time.Second)
|
||||||
|
conn.SetConnMaxLifetime(0)
|
||||||
if err := conn.Ping(); err != nil {
|
if err := conn.Ping(); err != nil {
|
||||||
_ = conn.Close()
|
_ = conn.Close()
|
||||||
return nil, fmt.Errorf("connect to %s: %w", name, err)
|
return nil, fmt.Errorf("connect to %s: %w", name, err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Set the dialect before migrating so the runner takes the MySQL path
|
// Set the dialect before migrating so the runner takes the MySQL path
|
||||||
// (per-statement, idempotent) rather than the SQLite transaction path.
|
// (per-statement, idempotent) rather than the SQLite transaction path.
|
||||||
Dialect = "mysql"
|
Dialect = "mysql"
|
||||||
|
|||||||
Reference in New Issue
Block a user