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())
|
||||
|
||||
sp := &spoolSync{}
|
||||
defer sp.close()
|
||||
|
||||
if *serveAddr != "" {
|
||||
if err := serve(*serveAddr, auth, sp, *dryRun); err != nil {
|
||||
if err := serve(*serveAddr, auth, *dryRun); err != nil {
|
||||
fail("serve: %v", err)
|
||||
}
|
||||
return
|
||||
@@ -87,7 +84,7 @@ func main() {
|
||||
}
|
||||
results := make([]result, 0, flag.NArg())
|
||||
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)
|
||||
@@ -95,50 +92,43 @@ func main() {
|
||||
enc.Encode(results)
|
||||
}
|
||||
|
||||
// spoolSync lazily logs into spooldb on first use, so its (slow) browser login
|
||||
// overlaps with the first image's weight read rather than blocking it. Every
|
||||
// site interaction is best-effort: failures are logged and the corresponding
|
||||
// output fields are simply omitted.
|
||||
type spoolSync struct {
|
||||
once sync.Once
|
||||
// spoolSession is a fresh, logged-in spooldb session used for a single image:
|
||||
// it owns one headless browser, does the read and the maybe-write, then closes.
|
||||
// Every site interaction is best-effort — failures are logged and the
|
||||
// corresponding output fields are simply omitted.
|
||||
type spoolSession struct {
|
||||
client *spooldb.Client
|
||||
err error
|
||||
}
|
||||
|
||||
func (s *spoolSync) login() (*spooldb.Client, error) {
|
||||
s.once.Do(func() {
|
||||
user, pass := os.Getenv("SPOOLDB_USER"), os.Getenv("SPOOLDB_PASS")
|
||||
if user == "" || pass == "" {
|
||||
s.err = fmt.Errorf("SPOOLDB_USER/SPOOLDB_PASS not set")
|
||||
return
|
||||
}
|
||||
c, err := spooldb.New()
|
||||
if err != nil {
|
||||
s.err = err
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
if err := c.Login(ctx, user, pass); err != nil {
|
||||
c.Close()
|
||||
s.err = fmt.Errorf("login: %w", err)
|
||||
return
|
||||
}
|
||||
s.client = c
|
||||
})
|
||||
return s.client, s.err
|
||||
// openSpoolSession launches a browser and logs into spooldb. It returns nil (and
|
||||
// logs) if credentials are missing or login fails, so the caller can carry on
|
||||
// with just the weight read.
|
||||
func openSpoolSession() *spoolSession {
|
||||
user, pass := os.Getenv("SPOOLDB_USER"), os.Getenv("SPOOLDB_PASS")
|
||||
if user == "" || pass == "" {
|
||||
slog.Warn("spooldb: skipping (SPOOLDB_USER/SPOOLDB_PASS not set)")
|
||||
return nil
|
||||
}
|
||||
c, err := spooldb.New()
|
||||
if err != nil {
|
||||
slog.Warn("spooldb: skipping (browser launch failed)", "err", err)
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
if err := c.Login(ctx, user, pass); err != nil {
|
||||
c.Close()
|
||||
slog.Warn("spooldb: skipping (login failed)", "err", err)
|
||||
return nil
|
||||
}
|
||||
return &spoolSession{client: c}
|
||||
}
|
||||
|
||||
// info reads the spool's current location and weights, or nil if unavailable.
|
||||
func (s *spoolSync) info(spoolID string) *spooldb.SpoolInfo {
|
||||
client, err := s.login()
|
||||
if err != nil {
|
||||
slog.Warn("spooldb: skipping spool lookup", "err", err)
|
||||
return nil
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
func (s *spoolSession) info(spoolID string) *spooldb.SpoolInfo {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
defer cancel()
|
||||
info, err := client.SpoolInfo(ctx, spoolID)
|
||||
info, err := s.client.SpoolInfo(ctx, spoolID)
|
||||
if err != nil {
|
||||
slog.Warn("spooldb: lookup failed", "spool", spoolID, "err", err)
|
||||
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.
|
||||
func (s *spoolSync) setTotal(spoolID string, grams float64) bool {
|
||||
client, err := s.login()
|
||||
if err != nil {
|
||||
slog.Warn("spooldb: skipping weight update", "err", err)
|
||||
return false
|
||||
}
|
||||
func (s *spoolSession) setTotal(spoolID string, grams float64) bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
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)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *spoolSync) close() {
|
||||
func (s *spoolSession) close() {
|
||||
if s.client != nil {
|
||||
s.client.Close()
|
||||
}
|
||||
@@ -170,17 +155,17 @@ func (s *spoolSync) close() {
|
||||
|
||||
// processImage reads one photo, capturing any failure in the result's Error
|
||||
// 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)
|
||||
if err != nil {
|
||||
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
|
||||
// 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)()
|
||||
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.URL = url
|
||||
|
||||
// Read the spool's current location/weight from the site concurrently with
|
||||
// the (slower) weight read off the photo.
|
||||
// Open a fresh spooldb session and read the spool's current location/weight,
|
||||
// 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 wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
info = sp.info(r.SpoolID)
|
||||
if sess = openSpoolSession(); sess != nil {
|
||||
info = sess.info(r.SpoolID)
|
||||
}
|
||||
}()
|
||||
|
||||
reading, err := claude.ReadWeight(img, auth)
|
||||
wg.Wait()
|
||||
if sess != nil {
|
||||
defer sess.close()
|
||||
}
|
||||
if info != nil {
|
||||
r.Location = info.Location
|
||||
r.PreviousWeight = &weightBreakdown{
|
||||
@@ -246,7 +238,7 @@ func processImg(img image.Image, name string, auth claude.Auth, sp *spoolSync, d
|
||||
if dryRun {
|
||||
slog.Info("spooldb: would update", "spool", r.SpoolID,
|
||||
"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
|
||||
slog.Info("spooldb: updated", "spool", r.SpoolID,
|
||||
"remaining_from", info.RemainingGrams, "remaining_to", newRemaining, "measured_total", reading.Weight)
|
||||
|
||||
Reference in New Issue
Block a user