use a fresh spooldb session per image instead of a shared one
This commit is contained in:
@@ -71,11 +71,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
slog.Debug("claude auth", "mechanism", auth.Name())
|
slog.Debug("claude auth", "mechanism", auth.Name())
|
||||||
|
|
||||||
sp := &spoolSync{}
|
|
||||||
defer sp.close()
|
|
||||||
|
|
||||||
if *serveAddr != "" {
|
if *serveAddr != "" {
|
||||||
if err := serve(*serveAddr, auth, sp, *dryRun); err != nil {
|
if err := serve(*serveAddr, auth, *dryRun); err != nil {
|
||||||
fail("serve: %v", err)
|
fail("serve: %v", err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -87,7 +84,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
results := make([]result, 0, flag.NArg())
|
results := make([]result, 0, flag.NArg())
|
||||||
for _, path := range flag.Args() {
|
for _, path := range flag.Args() {
|
||||||
results = append(results, processImage(path, auth, sp, *dryRun))
|
results = append(results, processImage(path, auth, *dryRun))
|
||||||
}
|
}
|
||||||
|
|
||||||
enc := json.NewEncoder(os.Stdout)
|
enc := json.NewEncoder(os.Stdout)
|
||||||
@@ -95,50 +92,43 @@ func main() {
|
|||||||
enc.Encode(results)
|
enc.Encode(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
// spoolSync lazily logs into spooldb on first use, so its (slow) browser login
|
// spoolSession is a fresh, logged-in spooldb session used for a single image:
|
||||||
// overlaps with the first image's weight read rather than blocking it. Every
|
// it owns one headless browser, does the read and the maybe-write, then closes.
|
||||||
// site interaction is best-effort: failures are logged and the corresponding
|
// Every site interaction is best-effort — failures are logged and the
|
||||||
// output fields are simply omitted.
|
// corresponding output fields are simply omitted.
|
||||||
type spoolSync struct {
|
type spoolSession struct {
|
||||||
once sync.Once
|
|
||||||
client *spooldb.Client
|
client *spooldb.Client
|
||||||
err error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *spoolSync) login() (*spooldb.Client, error) {
|
// openSpoolSession launches a browser and logs into spooldb. It returns nil (and
|
||||||
s.once.Do(func() {
|
// logs) if credentials are missing or login fails, so the caller can carry on
|
||||||
user, pass := os.Getenv("SPOOLDB_USER"), os.Getenv("SPOOLDB_PASS")
|
// with just the weight read.
|
||||||
if user == "" || pass == "" {
|
func openSpoolSession() *spoolSession {
|
||||||
s.err = fmt.Errorf("SPOOLDB_USER/SPOOLDB_PASS not set")
|
user, pass := os.Getenv("SPOOLDB_USER"), os.Getenv("SPOOLDB_PASS")
|
||||||
return
|
if user == "" || pass == "" {
|
||||||
}
|
slog.Warn("spooldb: skipping (SPOOLDB_USER/SPOOLDB_PASS not set)")
|
||||||
c, err := spooldb.New()
|
return nil
|
||||||
if err != nil {
|
}
|
||||||
s.err = err
|
c, err := spooldb.New()
|
||||||
return
|
if err != nil {
|
||||||
}
|
slog.Warn("spooldb: skipping (browser launch failed)", "err", err)
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
return nil
|
||||||
defer cancel()
|
}
|
||||||
if err := c.Login(ctx, user, pass); err != nil {
|
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||||
c.Close()
|
defer cancel()
|
||||||
s.err = fmt.Errorf("login: %w", err)
|
if err := c.Login(ctx, user, pass); err != nil {
|
||||||
return
|
c.Close()
|
||||||
}
|
slog.Warn("spooldb: skipping (login failed)", "err", err)
|
||||||
s.client = c
|
return nil
|
||||||
})
|
}
|
||||||
return s.client, s.err
|
return &spoolSession{client: c}
|
||||||
}
|
}
|
||||||
|
|
||||||
// info reads the spool's current location and weights, or nil if unavailable.
|
// info reads the spool's current location and weights, or nil if unavailable.
|
||||||
func (s *spoolSync) info(spoolID string) *spooldb.SpoolInfo {
|
func (s *spoolSession) info(spoolID string) *spooldb.SpoolInfo {
|
||||||
client, err := s.login()
|
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||||
if err != nil {
|
|
||||||
slog.Warn("spooldb: skipping spool lookup", "err", err)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
||||||
defer cancel()
|
defer cancel()
|
||||||
info, err := client.SpoolInfo(ctx, spoolID)
|
info, err := s.client.SpoolInfo(ctx, spoolID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("spooldb: lookup failed", "spool", spoolID, "err", err)
|
slog.Warn("spooldb: lookup failed", "spool", spoolID, "err", err)
|
||||||
return nil
|
return nil
|
||||||
@@ -147,22 +137,17 @@ func (s *spoolSync) info(spoolID string) *spooldb.SpoolInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setTotal writes a new total (with-spool) weight back to the site.
|
// setTotal writes a new total (with-spool) weight back to the site.
|
||||||
func (s *spoolSync) setTotal(spoolID string, grams float64) bool {
|
func (s *spoolSession) setTotal(spoolID string, grams float64) bool {
|
||||||
client, err := s.login()
|
|
||||||
if err != nil {
|
|
||||||
slog.Warn("spooldb: skipping weight update", "err", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
if err := client.SetTotalWeight(ctx, spoolID, grams); err != nil {
|
if err := s.client.SetTotalWeight(ctx, spoolID, grams); err != nil {
|
||||||
slog.Warn("spooldb: weight update failed", "spool", spoolID, "err", err)
|
slog.Warn("spooldb: weight update failed", "spool", spoolID, "err", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *spoolSync) close() {
|
func (s *spoolSession) close() {
|
||||||
if s.client != nil {
|
if s.client != nil {
|
||||||
s.client.Close()
|
s.client.Close()
|
||||||
}
|
}
|
||||||
@@ -170,17 +155,17 @@ func (s *spoolSync) close() {
|
|||||||
|
|
||||||
// processImage reads one photo, capturing any failure in the result's Error
|
// processImage reads one photo, capturing any failure in the result's Error
|
||||||
// field so a single bad image doesn't abort the whole batch.
|
// field so a single bad image doesn't abort the whole batch.
|
||||||
func processImage(path string, auth claude.Auth, sp *spoolSync, dryRun bool) result {
|
func processImage(path string, auth claude.Auth, dryRun bool) result {
|
||||||
img, err := loadImage(path)
|
img, err := loadImage(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result{Image: path, Error: fmt.Sprintf("load image: %v", err)}
|
return result{Image: path, Error: fmt.Sprintf("load image: %v", err)}
|
||||||
}
|
}
|
||||||
return processImg(img, path, auth, sp, dryRun)
|
return processImg(img, path, auth, dryRun)
|
||||||
}
|
}
|
||||||
|
|
||||||
// processImg runs the QR + weight + spooldb logic on a decoded image. name is
|
// processImg runs the QR + weight + spooldb logic on a decoded image. name is
|
||||||
// just a label for the output (a file path or upload filename).
|
// just a label for the output (a file path or upload filename).
|
||||||
func processImg(img image.Image, name string, auth claude.Auth, sp *spoolSync, dryRun bool) result {
|
func processImg(img image.Image, name string, auth claude.Auth, dryRun bool) result {
|
||||||
defer trace("image " + name)()
|
defer trace("image " + name)()
|
||||||
r := result{Image: name}
|
r := result{Image: name}
|
||||||
|
|
||||||
@@ -196,18 +181,25 @@ func processImg(img image.Image, name string, auth claude.Auth, sp *spoolSync, d
|
|||||||
r.SpoolID = spoolID(url)
|
r.SpoolID = spoolID(url)
|
||||||
r.URL = url
|
r.URL = url
|
||||||
|
|
||||||
// Read the spool's current location/weight from the site concurrently with
|
// Open a fresh spooldb session and read the spool's current location/weight,
|
||||||
// the (slower) weight read off the photo.
|
// concurrently with the (slower) weight read off the photo. The same session
|
||||||
|
// is reused for the maybe-write below, then closed.
|
||||||
|
var sess *spoolSession
|
||||||
var info *spooldb.SpoolInfo
|
var info *spooldb.SpoolInfo
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
info = sp.info(r.SpoolID)
|
if sess = openSpoolSession(); sess != nil {
|
||||||
|
info = sess.info(r.SpoolID)
|
||||||
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
reading, err := claude.ReadWeight(img, auth)
|
reading, err := claude.ReadWeight(img, auth)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
if sess != nil {
|
||||||
|
defer sess.close()
|
||||||
|
}
|
||||||
if info != nil {
|
if info != nil {
|
||||||
r.Location = info.Location
|
r.Location = info.Location
|
||||||
r.PreviousWeight = &weightBreakdown{
|
r.PreviousWeight = &weightBreakdown{
|
||||||
@@ -246,7 +238,7 @@ func processImg(img image.Image, name string, auth claude.Auth, sp *spoolSync, d
|
|||||||
if dryRun {
|
if dryRun {
|
||||||
slog.Info("spooldb: would update", "spool", r.SpoolID,
|
slog.Info("spooldb: would update", "spool", r.SpoolID,
|
||||||
"remaining_from", info.RemainingGrams, "remaining_to", newRemaining, "measured_total", reading.Weight)
|
"remaining_from", info.RemainingGrams, "remaining_to", newRemaining, "measured_total", reading.Weight)
|
||||||
} else if sp.setTotal(r.SpoolID, reading.Weight) {
|
} else if sess.setTotal(r.SpoolID, reading.Weight) {
|
||||||
updated = true
|
updated = true
|
||||||
slog.Info("spooldb: updated", "spool", r.SpoolID,
|
slog.Info("spooldb: updated", "spool", r.SpoolID,
|
||||||
"remaining_from", info.RemainingGrams, "remaining_to", newRemaining, "measured_total", reading.Weight)
|
"remaining_from", info.RemainingGrams, "remaining_to", newRemaining, "measured_total", reading.Weight)
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ import (
|
|||||||
//go:embed index.html
|
//go:embed index.html
|
||||||
var indexHTML []byte
|
var indexHTML []byte
|
||||||
|
|
||||||
// serve runs the mobile putaway web server. Requests are serialized because the
|
// serve runs the mobile putaway web server. Each request gets its own fresh
|
||||||
// shared spooldb browser session is single-threaded.
|
// spooldb session; requests are serialized so we never run several headless
|
||||||
func serve(addr string, auth claude.Auth, sp *spoolSync, dryRun bool) error {
|
// browsers at once.
|
||||||
|
func serve(addr string, auth claude.Auth, dryRun bool) error {
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
@@ -53,7 +54,7 @@ func serve(addr string, auth claude.Auth, sp *spoolSync, dryRun bool) error {
|
|||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
res := processImg(img, hdr.Filename, auth, sp, dryRun)
|
res := processImg(img, hdr.Filename, auth, dryRun)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
slog.Info("processed upload", "file", hdr.Filename, "spool", res.SpoolID, "dur", time.Since(start))
|
slog.Info("processed upload", "file", hdr.Filename, "spool", res.SpoolID, "dur", time.Since(start))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user