P5a review: outlines from named destinations; pool race; fast over-limit; revalidation under the limiter; Accept pdf

- pdftext/outline: resolve named destinations through the catalog /Dests and the /Names /Dests
  tree, resolve indirect /Dest, /A and /Title, and treat reader.Null (what Dict.Get returns for
  a missing key) as absent. Before this every LaTeX and Acrobat PDF had an empty outline.
  pdftest gains NamedDests; TestOutlineNamedDestinations.
- api: pool() always goes through its Once (data race on concurrent first use);
  TestDefaultPoolConcurrentFirstUse. Revalidation of a stale cache entry counts against the
  caller/host limit.
- pdftext/runner: output over MaxText kills the child at once instead of waiting for the
  deadline; TestRunnerTooMuchTextFailsFast.
- fetch: Accept includes application/pdf;q=0.8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-26 04:48:17 -07:00
co-authored by Claude Opus 5.5
parent 7e78de633f
commit aafc1d1c28
10 changed files with 287 additions and 65 deletions
+1 -1
View File
@@ -314,7 +314,7 @@ type v1Req struct {
JQ string `json:"jq"`
Recover bool `json:"recover"`
// Pages narrows a PDF digest to "2-3"; Grep matches lines in it. Optional; task-07.
// Pages ("41-60") and Grep (an RE2 pattern) narrow a PDF answer in clean mode; see pdfdoc.
Pages string `json:"pages"`
Grep string `json:"grep"`
}
+11 -4
View File
@@ -35,11 +35,12 @@ func (s *Server) runner() *pdftext.Runner {
// pool returns the extraction pool, built once and only when none is configured.
func (s *Server) pool() *pdfpool.Pool {
if s.PDFPool == nil {
s.pdfPoolOnce.Do(func() {
// Always through the Once: reading PDFPool outside it would race with the first writer.
s.pdfPoolOnce.Do(func() {
if s.PDFPool == nil {
s.PDFPool = pdfpool.New(2, 4, 20*time.Second)
})
}
}
})
return s.PDFPool
}
@@ -158,7 +159,13 @@ func (s *Server) pdfFromCache(ctx context.Context, c *auth.Caller, rawURL string
res, doc := s.pdfFromEntry(rawURL, e)
return res, doc, true, nil
}
// A revalidation is an upstream request like any other: it counts against the caller's and
// the host's limits.
if err := s.acquire(c.Name, rawURL); err != nil {
return nil, nil, false, err
}
res, err := s.Fetch.FetchIf(ctx, rawURL, e.ETag, e.LastModified)
s.release(c.Name, rawURL)
if err != nil {
return nil, nil, false, err
}
+30
View File
@@ -0,0 +1,30 @@
package api
// Owner review (P5a): the default pool is built once, safely, when concurrent first requests
// arrive on a server with no pool configured (run under -race).
import (
"sync"
"testing"
)
func TestDefaultPoolConcurrentFirstUse(t *testing.T) {
s, _ := setupAPI(t, nil)
var wg sync.WaitGroup
got := make(chan any, 16)
for i := 0; i < 16; i++ {
wg.Add(1)
go func() { defer wg.Done(); got <- s.pool() }()
}
wg.Wait()
close(got)
var first any
for p := range got {
if first == nil {
first = p
}
if p != first || p == nil {
t.Fatal("pool() returned different or nil pools")
}
}
}