db reconnect
This commit is contained in:
parent
895849376e
commit
7f6262b470
@ -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 == "" {
|
||||
|
||||
1
db/db.go
1
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
|
||||
}
|
||||
|
||||
|
||||
32
errorhandlers/errorhandlers.go
Normal file
32
errorhandlers/errorhandlers.go
Normal 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
54
handlers/db_helpers.go
Normal 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
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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
15
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user