From 7f6262b470ac1a87c7eef52b24d437580dd12c5b Mon Sep 17 00:00:00 2001 From: yurii Date: Fri, 23 Jan 2026 17:15:45 +0000 Subject: [PATCH] db reconnect --- config/config.go | 6 ++-- db/db.go | 1 + errorhandlers/errorhandlers.go | 32 ++++++++++++++++++++ handlers/db_helpers.go | 54 ++++++++++++++++++++++++++++++++++ handlers/handlers.go | 50 +++++++++++++++++++------------ handlers/http_helpers.go | 21 ------------- main.go | 15 +++++----- 7 files changed, 130 insertions(+), 49 deletions(-) create mode 100644 errorhandlers/errorhandlers.go create mode 100644 handlers/db_helpers.go diff --git a/config/config.go b/config/config.go index 408ee6b..4c47b8a 100644 --- a/config/config.go +++ b/config/config.go @@ -5,7 +5,7 @@ import ( "os" "strings" - "gitea.futuresens.co.uk/futuresens/hardlink/handlers" + "gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers" log "github.com/sirupsen/logrus" yaml "gopkg.in/yaml.v3" ) @@ -49,7 +49,7 @@ func ReadHardlinkConfig() ConfigRec { if cfg.LockType == "" { err = fmt.Errorf("LockType is required in %s", configName) - handlers.FatalError(err) + errorhandlers.FatalError(err) } cfg.LockType = strings.ToLower(cfg.LockType) @@ -81,7 +81,7 @@ func ReadPreauthReleaserConfig() ConfigRec { if cfg.Dbport <= 0 || cfg.Dbuser == "" || cfg.Dbname == "" || cfg.Dbpassword == "" { err = fmt.Errorf("Database config (dbport, dbuser, dbname, dbpassword) are required in %s", configName) - handlers.FatalError(err) + errorhandlers.FatalError(err) } if cfg.LogDir == "" { diff --git a/db/db.go b/db/db.go index 3ff2176..783c3fe 100644 --- a/db/db.go +++ b/db/db.go @@ -198,3 +198,4 @@ WHERE TxnReference = @TxnReference AND Released = 0; log.Infof("Marked preauth %s released at %s", txnReference, releasedAt.Format(time.RFC3339)) return nil } + diff --git a/errorhandlers/errorhandlers.go b/errorhandlers/errorhandlers.go new file mode 100644 index 0000000..1c5cd48 --- /dev/null +++ b/errorhandlers/errorhandlers.go @@ -0,0 +1,32 @@ +package errorhandlers + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + + "gitea.futuresens.co.uk/futuresens/cmstypes" + log "github.com/sirupsen/logrus" +) + +const serviceName = "hardlink" + +// writeError is a helper to send a JSON error and HTTP status in one go. +func WriteError(w http.ResponseWriter, status int, msg string) { + theResponse := cmstypes.StatusRec{ + Code: status, + Message: msg, + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + json.NewEncoder(w).Encode(theResponse) +} + +func FatalError(err error) { + fmt.Println(err.Error()) + log.Errorf(err.Error()) + fmt.Println(". Press Enter to exit...") + fmt.Scanln() + os.Exit(1) +} diff --git a/handlers/db_helpers.go b/handlers/db_helpers.go new file mode 100644 index 0000000..ad960d1 --- /dev/null +++ b/handlers/db_helpers.go @@ -0,0 +1,54 @@ +package handlers + +import ( + "context" + "database/sql" + "time" + + "gitea.futuresens.co.uk/futuresens/hardlink/db" +) + +func (app *App) getDB(ctx context.Context) (*sql.DB, error) { + app.dbMu.Lock() + defer app.dbMu.Unlock() + + // Fast path: db exists and is alive + if app.db != nil { + pingCtx, cancel := context.WithTimeout(ctx, 1*time.Second) + defer cancel() + + if err := app.db.PingContext(pingCtx); err == nil { + return app.db, nil + } + + // stale handle + _ = app.db.Close() + app.db = nil + } + + // Reconnect once, bounded + dialCtx, cancel := context.WithTimeout(ctx, 3*time.Second) + defer cancel() + + dbConn, err := db.InitMSSQL( + app.cfg.Dbport, + app.cfg.Dbuser, + app.cfg.Dbpassword, + app.cfg.Dbname, + ) + if err != nil { + return nil, err + } + + // Optional ping (InitMSSQL already pings, but this keeps semantics explicit) + pingCtx, cancel2 := context.WithTimeout(dialCtx, 1*time.Second) + defer cancel2() + + if err := dbConn.PingContext(pingCtx); err != nil { + _ = dbConn.Close() + return nil, err + } + + app.db = dbConn + return app.db, nil +} diff --git a/handlers/handlers.go b/handlers/handlers.go index 1bd476b..5b73643 100644 --- a/handlers/handlers.go +++ b/handlers/handlers.go @@ -8,13 +8,16 @@ import ( "io" "net/http" "strings" + "sync" "time" "github.com/tarm/serial" "gitea.futuresens.co.uk/futuresens/cmstypes" + "gitea.futuresens.co.uk/futuresens/hardlink/config" "gitea.futuresens.co.uk/futuresens/hardlink/db" "gitea.futuresens.co.uk/futuresens/hardlink/dispenser" + "gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers" "gitea.futuresens.co.uk/futuresens/hardlink/lockserver" "gitea.futuresens.co.uk/futuresens/hardlink/payment" "gitea.futuresens.co.uk/futuresens/hardlink/printer" @@ -28,14 +31,17 @@ type App struct { lockserver lockserver.LockServer isPayment bool db *sql.DB + cfg *config.ConfigRec + dbMu sync.Mutex } -func NewApp(dispPort *serial.Port, lockType, encoderAddress string, db *sql.DB, isPayment bool) *App { +func NewApp(dispPort *serial.Port, lockType, encoderAddress string, db *sql.DB, cfg *config.ConfigRec) *App { return &App{ - isPayment: isPayment, + isPayment: cfg.IsPayment, dispPort: dispPort, - lockserver: lockserver.NewLockServer(lockType, encoderAddress, FatalError), + lockserver: lockserver.NewLockServer(lockType, encoderAddress, errorhandlers.FatalError), db: db, + cfg: cfg, } } @@ -131,8 +137,16 @@ func (app *App) takePreauthorization(w http.ResponseWriter, r *http.Request) { theResponse.Status = result.Status theResponse.Data, save = payment.BuildPreauthRedirectURL(result.Fields) if save { - db.InsertPreauth(r.Context(), app.db, result.Fields, theRequest.CheckoutDate) + dbConn, err := app.getDB(r.Context()) + if err != nil { + log.WithError(err).Warn("DB unavailable; preauth not stored") + } else { + if err := db.InsertPreauth(r.Context(), dbConn, result.Fields, theRequest.CheckoutDate); err != nil { + log.WithError(err).Warn("Failed to store preauth in DB") + } + } } + writeTransactionResult(w, http.StatusOK, theResponse) } @@ -241,19 +255,19 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) { log.Println("issueDoorCard called") if r.Method != http.MethodPost { - writeError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST") + errorhandlers.WriteError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST") return } defer r.Body.Close() if ct := r.Header.Get("Content-Type"); ct != "application/json" { - writeError(w, http.StatusUnsupportedMediaType, "Content-Type must be application/json") + errorhandlers.WriteError(w, http.StatusUnsupportedMediaType, "Content-Type must be application/json") return } if err := json.NewDecoder(r.Body).Decode(&doorReq); err != nil { logging.Error(serviceName, err.Error(), "ReadJSON", string(op), "", "", 0) - writeError(w, http.StatusBadRequest, "Invalid JSON payload: "+err.Error()) + errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid JSON payload: "+err.Error()) return } @@ -261,13 +275,13 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) { checkIn, err := time.Parse(types.CustomLayout, doorReq.CheckinTime) if err != nil { logging.Error(serviceName, err.Error(), "Invalid checkinTime format", string(op), "", "", 0) - writeError(w, http.StatusBadRequest, "Invalid checkinTime format: "+err.Error()) + errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid checkinTime format: "+err.Error()) return } checkOut, err := time.Parse(types.CustomLayout, doorReq.CheckoutTime) if err != nil { logging.Error(serviceName, err.Error(), "Invalid checkoutTime format", string(op), "", "", 0) - writeError(w, http.StatusBadRequest, "Invalid checkoutTime format: "+err.Error()) + errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid checkoutTime format: "+err.Error()) return } @@ -275,10 +289,10 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) { if status, err := dispenser.DispenserSequence(app.dispPort); err != nil { if status != "" { logging.Error(serviceName, status, "Dispense error", string(op), "", "", 0) - writeError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error()) + errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error()) } else { logging.Error(serviceName, err.Error(), "Dispense error", string(op), "", "", 0) - writeError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error()+"; check card stock") + errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispense error: "+err.Error()+"; check card stock") } return } else { @@ -292,7 +306,7 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) { err = app.lockserver.LockSequence() if err != nil { logging.Error(serviceName, err.Error(), "Key encoding", string(op), "", "", 0) - writeError(w, http.StatusBadGateway, err.Error()) + errorhandlers.WriteError(w, http.StatusBadGateway, err.Error()) dispenser.CardOutOfMouth(app.dispPort) return } @@ -300,7 +314,7 @@ func (app *App) issueDoorCard(w http.ResponseWriter, r *http.Request) { // final dispenser steps if status, err := dispenser.CardOutOfMouth(app.dispPort); err != nil { logging.Error(serviceName, err.Error(), "Dispenser eject error", string(op), "", "", 0) - writeError(w, http.StatusServiceUnavailable, "Dispenser eject error: "+err.Error()) + errorhandlers.WriteError(w, http.StatusServiceUnavailable, "Dispenser eject error: "+err.Error()) return } else { log.Info(status) @@ -328,32 +342,32 @@ func (app *App) printRoomTicket(w http.ResponseWriter, r *http.Request) { } log.Println("printRoomTicket called") if r.Method != http.MethodPost { - writeError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST") + errorhandlers.WriteError(w, http.StatusMethodNotAllowed, "Method not allowed; use POST") return } if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "xml") { - writeError(w, http.StatusUnsupportedMediaType, "Content-Type must be application/xml") + errorhandlers.WriteError(w, http.StatusUnsupportedMediaType, "Content-Type must be application/xml") return } defer r.Body.Close() if err := xml.NewDecoder(r.Body).Decode(&roomDetails); err != nil { logging.Error(serviceName, err.Error(), "ReadXML", string(op), "", "", 0) - writeError(w, http.StatusBadRequest, "Invalid XML payload: "+err.Error()) + errorhandlers.WriteError(w, http.StatusBadRequest, "Invalid XML payload: "+err.Error()) return } data, err := printer.BuildRoomTicket(roomDetails) if err != nil { logging.Error(serviceName, err.Error(), "BuildRoomTicket", string(op), "", "", 0) - writeError(w, http.StatusInternalServerError, "BuildRoomTicket failed: "+err.Error()) + errorhandlers.WriteError(w, http.StatusInternalServerError, "BuildRoomTicket failed: "+err.Error()) return } // Send to the Windows Epson TM-T82II via the printer package if err := printer.SendToPrinter(data); err != nil { logging.Error(serviceName, err.Error(), "printRoomTicket", "printRoomTicket", "", "", 0) - writeError(w, http.StatusInternalServerError, "Print failed: "+err.Error()) + errorhandlers.WriteError(w, http.StatusInternalServerError, "Print failed: "+err.Error()) return } diff --git a/handlers/http_helpers.go b/handlers/http_helpers.go index 640cb73..0cfc279 100644 --- a/handlers/http_helpers.go +++ b/handlers/http_helpers.go @@ -2,28 +2,14 @@ package handlers import ( "encoding/json" - "fmt" "net/http" - "os" "gitea.futuresens.co.uk/futuresens/cmstypes" "gitea.futuresens.co.uk/futuresens/logging" - log "github.com/sirupsen/logrus" ) const serviceName = "hardlink" -// writeError is a helper to send a JSON error and HTTP status in one go. -func writeError(w http.ResponseWriter, status int, msg string) { - theResponse := cmstypes.StatusRec{ - Code: status, - Message: msg, - } - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - json.NewEncoder(w).Encode(theResponse) -} - func writeTransactionResult(w http.ResponseWriter, status int, theResponse cmstypes.ResponseRec) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) @@ -32,10 +18,3 @@ func writeTransactionResult(w http.ResponseWriter, status int, theResponse cmsty } } -func FatalError(err error) { - fmt.Println(err.Error()) - log.Errorf(err.Error()) - fmt.Println(". Press Enter to exit...") - fmt.Scanln() - os.Exit(1) -} diff --git a/main.go b/main.go index e002cbd..da54d60 100644 --- a/main.go +++ b/main.go @@ -19,6 +19,7 @@ import ( "gitea.futuresens.co.uk/futuresens/hardlink/config" "gitea.futuresens.co.uk/futuresens/hardlink/dispenser" "gitea.futuresens.co.uk/futuresens/hardlink/handlers" + "gitea.futuresens.co.uk/futuresens/hardlink/errorhandlers" "gitea.futuresens.co.uk/futuresens/hardlink/lockserver" "gitea.futuresens.co.uk/futuresens/hardlink/logging" "gitea.futuresens.co.uk/futuresens/hardlink/printer" @@ -51,7 +52,7 @@ func main() { dispenser.Address = []byte(config.DispenserAdrr) dispHandle, err = dispenser.InitializeDispenser() if err != nil { - handlers.FatalError(err) + errorhandlers.FatalError(err) } defer dispHandle.Close() @@ -59,7 +60,7 @@ func main() { if err != nil { if len(status) == 0 { err = fmt.Errorf("%s; wrong dispenser address: %s", err, config.DispenserAdrr) - handlers.FatalError(err) + errorhandlers.FatalError(err) } else { fmt.Println(status) fmt.Println(err.Error()) @@ -100,7 +101,7 @@ func main() { } // Create App and wire routes - app := handlers.NewApp(dispHandle, config.LockType, config.EncoderAddress, database, config.IsPayment) + app := handlers.NewApp(dispHandle, config.LockType, config.EncoderAddress, database, &config) mux := http.NewServeMux() app.RegisterRoutes(mux) @@ -109,7 +110,7 @@ func main() { log.Infof("Starting HTTP server on http://localhost%s", addr) fmt.Printf("Starting HTTP server on http://localhost%s", addr) if err := http.ListenAndServe(addr, mux); err != nil { - handlers.FatalError(err) + errorhandlers.FatalError(err) } } @@ -120,12 +121,12 @@ func readTicketLayout() printer.LayoutOptions { // 1) Read the file data, err := os.ReadFile(layoutName) if err != nil { - handlers.FatalError(fmt.Errorf("failed to read %s: %v", layoutName, err)) + errorhandlers.FatalError(fmt.Errorf("failed to read %s: %v", layoutName, err)) } // 2) Unmarshal into your struct if err := xml.Unmarshal(data, &layout); err != nil { - handlers.FatalError(fmt.Errorf("failed to parse %s: %v", layoutName, err)) + errorhandlers.FatalError(fmt.Errorf("failed to parse %s: %v", layoutName, err)) } return layout @@ -144,7 +145,7 @@ func startChipDnaClient() { cmd, err := startClient() if err != nil { - handlers.FatalError(err) + errorhandlers.FatalError(err) } // Restart loop