db reconnect

This commit is contained in:
yurii 2026-01-23 17:15:45 +00:00
parent 895849376e
commit 7f6262b470
7 changed files with 130 additions and 49 deletions

View File

@ -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 == "" {

View File

@ -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
}

View File

@ -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)
}

54
handlers/db_helpers.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

15
main.go
View File

@ -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