From 0285a9b0bd638296696bcb6ce49eef6138a803d9 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Wed, 18 Mar 2020 16:14:05 -0400 Subject: [PATCH 1/2] Show 503 page on collections under high load This acknowledges "too many connections" and "max user connections" errors in MySQL and propagates the error up the chain so we can notify the user and return the correct HTTP code. --- database-no-sqlite.go | 10 ++++++++++ database.go | 4 ++++ errors.go | 2 ++ handle.go | 7 +++++++ pages/503.tmpl | 7 +++++++ 5 files changed, 30 insertions(+) create mode 100644 pages/503.tmpl diff --git a/database-no-sqlite.go b/database-no-sqlite.go index 03d1a32..f2c7ffc 100644 --- a/database-no-sqlite.go +++ b/database-no-sqlite.go @@ -40,3 +40,13 @@ func (db *datastore) isIgnorableError(err error) bool { return false } + +func (db *datastore) isHighLoadError(err error) bool { + if db.driverName == driverMySQL { + if mysqlErr, ok := err.(*mysql.MySQLError); ok { + return mysqlErr.Number == mySQLErrMaxUserConns || mysqlErr.Number == mySQLErrTooManyConns + } + } + + return false +} diff --git a/database.go b/database.go index f5e4564..3ed0920 100644 --- a/database.go +++ b/database.go @@ -39,6 +39,8 @@ import ( const ( mySQLErrDuplicateKey = 1062 mySQLErrCollationMix = 1267 + mySQLErrTooManyConns = 1040 + mySQLErrMaxUserConns = 1203 driverMySQL = "mysql" driverSQLite = "sqlite3" @@ -793,6 +795,8 @@ func (db *datastore) GetCollectionBy(condition string, value interface{}) (*Coll switch { case err == sql.ErrNoRows: return nil, impart.HTTPError{http.StatusNotFound, "Collection doesn't exist."} + case db.isHighLoadError(err): + return nil, ErrUnavailable case err != nil: log.Error("Failed selecting from collections: %v", err) return nil, err diff --git a/errors.go b/errors.go index b62fc9e..579386b 100644 --- a/errors.go +++ b/errors.go @@ -37,6 +37,8 @@ var ( ErrInternalGeneral = impart.HTTPError{http.StatusInternalServerError, "The humans messed something up. They've been notified."} ErrInternalCookieSession = impart.HTTPError{http.StatusInternalServerError, "Could not get cookie session."} + ErrUnavailable = impart.HTTPError{http.StatusServiceUnavailable, "Service temporarily unavailable due to high load."} + ErrCollectionNotFound = impart.HTTPError{http.StatusNotFound, "Collection doesn't exist."} ErrCollectionGone = impart.HTTPError{http.StatusGone, "This blog was unpublished."} ErrCollectionPageNotFound = impart.HTTPError{http.StatusNotFound, "Collection page doesn't exist."} diff --git a/handle.go b/handle.go index 0fcc483..4915420 100644 --- a/handle.go +++ b/handle.go @@ -83,6 +83,7 @@ type ErrorPages struct { NotFound *template.Template Gone *template.Template InternalServerError *template.Template + UnavailableError *template.Template Blank *template.Template } @@ -94,6 +95,7 @@ func NewHandler(apper Apper) *Handler { NotFound: template.Must(template.New("").Parse("{{define \"base\"}}404

Not found.

{{end}}")), Gone: template.Must(template.New("").Parse("{{define \"base\"}}410

Gone.

{{end}}")), InternalServerError: template.Must(template.New("").Parse("{{define \"base\"}}500

Internal server error.

{{end}}")), + UnavailableError: template.Must(template.New("").Parse("{{define \"base\"}}503

Service is temporarily unavailable.

{{end}}")), Blank: template.Must(template.New("").Parse("{{define \"base\"}}{{.Title}}

{{.Content}}

{{end}}")), }, sessionStore: apper.App().SessionStore(), @@ -111,6 +113,7 @@ func NewWFHandler(apper Apper) *Handler { NotFound: pages["404-general.tmpl"], Gone: pages["410.tmpl"], InternalServerError: pages["500.tmpl"], + UnavailableError: pages["503.tmpl"], Blank: pages["blank.tmpl"], }) return h @@ -763,6 +766,10 @@ func (h *Handler) handleHTTPError(w http.ResponseWriter, r *http.Request, err er log.Info("handleHTTPErorr internal error render") h.errors.InternalServerError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) return + } else if err.Status == http.StatusServiceUnavailable { + w.WriteHeader(err.Status) + h.errors.UnavailableError.ExecuteTemplate(w, "base", pageForReq(h.app.App(), r)) + return } else if err.Status == http.StatusAccepted { impart.WriteSuccess(w, "", err.Status) return diff --git a/pages/503.tmpl b/pages/503.tmpl new file mode 100644 index 0000000..70c6c78 --- /dev/null +++ b/pages/503.tmpl @@ -0,0 +1,7 @@ +{{define "head"}}Temporarily Unavailable — {{.SiteMetaName}}{{end}} +{{define "content"}} +
+

The words aren't coming to me. 🗅

+

We couldn't serve this page due to high server load. This should only be temporary.

+
+{{end}} From 07debec8d5b6957b736ad07cfb7dc081d38fe024 Mon Sep 17 00:00:00 2001 From: Matt Baer Date: Fri, 27 Mar 2020 11:48:20 -0400 Subject: [PATCH 2/2] Add new err func to wflib and sqlite builds --- database-lib.go | 4 ++++ database-sqlite.go | 12 +++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/database-lib.go b/database-lib.go index b6b4be2..8b28577 100644 --- a/database-lib.go +++ b/database-lib.go @@ -22,3 +22,7 @@ func (db *datastore) isDuplicateKeyErr(err error) bool { func (db *datastore) isIgnorableError(err error) bool { return false } + +func (db *datastore) isHighLoadError(err error) bool { + return false +} diff --git a/database-sqlite.go b/database-sqlite.go index bd77e6a..10e701e 100644 --- a/database-sqlite.go +++ b/database-sqlite.go @@ -1,7 +1,7 @@ // +build sqlite,!wflib /* - * Copyright © 2019 A Bunch Tell LLC. + * Copyright © 2019-2020 A Bunch Tell LLC. * * This file is part of WriteFreely. * @@ -60,3 +60,13 @@ func (db *datastore) isIgnorableError(err error) bool { return false } + +func (db *datastore) isHighLoadError(err error) bool { + if db.driverName == driverMySQL { + if mysqlErr, ok := err.(*mysql.MySQLError); ok { + return mysqlErr.Number == mySQLErrMaxUserConns || mysqlErr.Number == mySQLErrTooManyConns + } + } + + return false +}