mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-11 22:25:41 +08:00
feat(ingestion): mirror Go pipeline progress into the document table;
harden resume guards
- pipeline: bind the owning document via WithDocumentID; after each
TrackProgress event aggregate ingestion_task_log progress and mirror
progress/run/progress_msg back into the document table, so GET
/api/v1/datasets/{dataset_id}/documents reflects live Go pipeline
progress without a bespoke endpoint.
- canvas: extend the S3 resume guard to reject legacy no-op nodes (e.g.
ExitLoop) so component_total equals the count of progress-reporting
components and the aggregate percent can reach 100%.
- runtime/canvas: route progress through TrackProgress; add interrupt
test coverage (r3_interrupt_test.go).
- dao/entity: add IngestionTask.DocumentID column and AggregateProgress
support used by the mirror; IngestionTaskLog keeps a Checkpoint column
alongside the progress fields.
feat(deepdoc): cache DocAnalyzer inference results in Redis (1h TTL)
- Redis-backed DocAnalyzerCache decorator over inference.Client; cache
key = "ddoc:cache:<method>:" + sha256 of the JPEG-encoded image bytes
(deterministic).
- TTL = 1h; hits skip the inner HTTP call and return cached JSON; inner
errors are not cached.
refactor(deepdoc): align figure cropping with Python cropout + bounded
page caches
- CropSectionByDLA mirrors Python cropout: best-overlap DLA
figure/equation region, fallback to section bbox per page, vertical
concat on gray background.
- sliding-window page-image cache bounds peak memory to the recent
window instead of the whole PDF.
- rename DLADebug -> DLARegions across parser/chunker/tests.
refactor(parser): drop lib_type selector; align NewXxxParser with
NewPDFParser
- remove config["lib_type"] lookup and the libType param/field/switch
from all nine constructors; surface the CGO-required error at
ParseWithResult time instead of construction time; drop resolveLibType,
its test, and the four lib_type constants.
feat(utility): add a reusable workerpool for bounded concurrent
execution
- internal/utility/workerpool.go (+ tests).
refactor: translate Chinese prose comments to English in non-harness Go
files.
chore: upgrade github.com/cloudwego/eino from v0.9.9 to v0.9.12.
225 lines
7.0 KiB
Go
225 lines
7.0 KiB
Go
package pdf
|
||
|
||
import (
|
||
"context"
|
||
"image"
|
||
"runtime"
|
||
"sync"
|
||
|
||
pdf "ragflow/internal/deepdoc/parser/pdf/type"
|
||
"ragflow/internal/utility"
|
||
)
|
||
|
||
// ── Internal concurrency guards ──────────────────────────────────────────
|
||
//
|
||
// Real page parallelism exposes the parser to multiplicative DeepDoc
|
||
// inference fan-out (DLA + TSR per table region + OCR per region).
|
||
// deepInfLimiter (deepdoc inference limiter) bounds the worst-case
|
||
// concurrent inference work a single document can drive. It is
|
||
// parser-owned, not user-configurable, so callers do not have to reason
|
||
// about the knob.
|
||
//
|
||
// Native PDFium access (RenderPage / ExtractChars / PageSize / outlines)
|
||
// is serialized by a process-wide mutex in package pdfsync, shared by
|
||
// both the cgo pdfium binding and the Rust pdf_oxide binding — PDFium is
|
||
// not thread-safe for any call, even across different documents, so the
|
||
// mutex (not a per-Parser limiter) is the correct guard. See
|
||
// pdfsync/pdfsync.go.
|
||
|
||
const (
|
||
// defaultdeepInfCapacity bounds DeepDoc (deepdoc) inference calls
|
||
// (DLA, TSR, OCR).
|
||
defaultdeepInfCapacity = 8
|
||
)
|
||
|
||
// deepInfLimiter bounds concurrent DeepDoc (deepdoc) inference calls emitted
|
||
// by per-page workers. acquire blocks until a slot is available or the
|
||
// context is cancelled; release must be called in a defer.
|
||
type deepInfLimiter struct {
|
||
sem chan struct{}
|
||
}
|
||
|
||
func newdeepInfLimiter(capacity int) *deepInfLimiter {
|
||
if capacity <= 0 {
|
||
capacity = defaultdeepInfCapacity
|
||
}
|
||
return &deepInfLimiter{sem: make(chan struct{}, capacity)}
|
||
}
|
||
|
||
func (l *deepInfLimiter) acquire(ctx context.Context) error {
|
||
if l == nil {
|
||
return nil
|
||
}
|
||
select {
|
||
case <-ctx.Done():
|
||
return ctx.Err()
|
||
case l.sem <- struct{}{}:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
func (l *deepInfLimiter) release() {
|
||
if l == nil {
|
||
return
|
||
}
|
||
<-l.sem
|
||
}
|
||
|
||
// withSlot acquires a slot, runs fn, and releases the slot. fn's error
|
||
// is propagated unchanged.
|
||
func (l *deepInfLimiter) withSlot(ctx context.Context, fn func() error) error {
|
||
if err := l.acquire(ctx); err != nil {
|
||
return err
|
||
}
|
||
defer l.release()
|
||
return fn()
|
||
}
|
||
|
||
// ── Parser-owned limiter accessors ────────────────────────────────────────
|
||
|
||
// pageTask holds the per-page work handed to the shared worker pool.
|
||
type pageTask struct {
|
||
parser *Parser
|
||
engine pdf.PDFEngine
|
||
pageNumber int
|
||
docAnalyzer pdf.DocAnalyzer
|
||
tb pdf.TableBuilder
|
||
}
|
||
|
||
const pageWorkerQueueFactor = 4
|
||
|
||
var (
|
||
pagePoolOnce sync.Once
|
||
pagePool *utility.WorkerPool[pageTask, pageResult]
|
||
)
|
||
|
||
func parserPageWorkerPool() *utility.WorkerPool[pageTask, pageResult] {
|
||
pagePoolOnce.Do(func() {
|
||
workers := runtime.GOMAXPROCS(0) * 2
|
||
if workers <= 0 {
|
||
workers = 1
|
||
}
|
||
pagePool = utility.NewWorkerPool(workers, workers*pageWorkerQueueFactor,
|
||
func(ctx context.Context, task pageTask) (pageResult, error) {
|
||
return task.parser.processPage(ctx, task.engine, task.pageNumber,
|
||
task.docAnalyzer, task.tb), nil
|
||
})
|
||
})
|
||
return pagePool
|
||
}
|
||
|
||
// PageWorkerPoolStats returns process-wide stats for the shared PDF page worker pool.
|
||
func PageWorkerPoolStats() utility.WorkerPoolStats {
|
||
return parserPageWorkerPool().Stats()
|
||
}
|
||
|
||
// SetPageWorkerPoolSize adjusts the process-wide PDF page worker pool size.
|
||
func SetPageWorkerPoolSize(workers int) {
|
||
parserPageWorkerPool().Resize(workers)
|
||
}
|
||
|
||
// limiters returns the parser's lazily-initialized DeepDoc inference
|
||
// limiter. The returned limiter is shared across ParseRaw calls (and
|
||
// per-page workers) so test or production callers that reuse a Parser
|
||
// do not pile up extra slots. Creation is guarded by deepInfOnce so a
|
||
// Parser shared across goroutines initializes the slot channel once.
|
||
func (p *Parser) limiters() *deepInfLimiter {
|
||
p.deepInfOnce.Do(func() {
|
||
p.deepInf = newdeepInfLimiter(0)
|
||
})
|
||
return p.deepInf
|
||
}
|
||
|
||
// ── Wrapped calls used by the parser pipeline ─────────────────────────────
|
||
|
||
// renderPageToImage renders a page at the default DLA DPI. Native PDFium
|
||
// access inside the engine is serialized by the process-wide pdfsync.Mu
|
||
// (see pdfsync/pdfsync.go), so no per-Parser engine limiter is needed here.
|
||
func (p *Parser) renderPageToImage(ctx context.Context, eng pdf.PDFEngine, pageNum int) (image.Image, error) {
|
||
return RenderPageToImage(eng, pageNum)
|
||
}
|
||
|
||
// renderAtDPI invokes the engine's DPI-parameterized render path. As with
|
||
// renderPageToImage, native PDFium serialization is handled by pdfsync.Mu.
|
||
func (p *Parser) renderAtDPI(ctx context.Context, eng pdf.PDFEngine, pageNum int, dpi float64) (image.Image, error) {
|
||
return eng.RenderPageImage(pageNum, dpi)
|
||
}
|
||
|
||
// inferDLA acquires the inference limiter slot before invoking the
|
||
// per-page DLA call. Page workers and enrichOnePageWithDeepDoc callers
|
||
// route through this wrapper so DLA fan-out cannot overwhelm the
|
||
// DeepDoc service independently of the page worker pool size.
|
||
func (p *Parser) inferDLA(ctx context.Context, doc pdf.DocAnalyzer, pageImg image.Image) ([]pdf.DLARegion, error) {
|
||
if doc == nil || !doc.Health() {
|
||
return nil, nil
|
||
}
|
||
var regions []pdf.DLARegion
|
||
err := p.limiters().withSlot(ctx, func() error {
|
||
r, callErr := doc.DLA(ctx, pageImg)
|
||
if callErr != nil {
|
||
return callErr
|
||
}
|
||
regions = r
|
||
return nil
|
||
})
|
||
return regions, err
|
||
}
|
||
|
||
// inferTSR acquires the inference limiter slot before invoking TSR for
|
||
// a single cropped table region. TSR can be called once per detected
|
||
// DLA table region, so without this wrapper the worst-case
|
||
// (DlaRegion × pool worker count) fan-out could overwhelm the service.
|
||
func (p *Parser) inferTSR(ctx context.Context, tb pdf.TableBuilder, cropped image.Image) ([]pdf.TSRCell, error) {
|
||
if tb == nil {
|
||
return nil, nil
|
||
}
|
||
var cells []pdf.TSRCell
|
||
err := p.limiters().withSlot(ctx, func() error {
|
||
c, callErr := tb.DetectCells(ctx, cropped)
|
||
if callErr != nil {
|
||
return callErr
|
||
}
|
||
cells = c
|
||
return nil
|
||
})
|
||
return cells, err
|
||
}
|
||
|
||
// inferOCRDetect routes doc.OCRDetect through the inference limiter.
|
||
// ocrMergeChars and ocrDetectAndRecognize callers should funnel
|
||
// through this helper.
|
||
func (p *Parser) inferOCRDetect(ctx context.Context, doc pdf.DocAnalyzer, pageImg image.Image) ([]pdf.OCRBox, error) {
|
||
if doc == nil || !doc.Health() {
|
||
return nil, nil
|
||
}
|
||
var boxes []pdf.OCRBox
|
||
err := p.limiters().withSlot(ctx, func() error {
|
||
b, callErr := doc.OCRDetect(ctx, pageImg)
|
||
if callErr != nil {
|
||
return callErr
|
||
}
|
||
boxes = b
|
||
return nil
|
||
})
|
||
return boxes, err
|
||
}
|
||
|
||
// inferOCRRecognize routes doc.OCRRecognize through the inference
|
||
// limiter. Per-region OCR fallback paths (buildTextBoxes, ocrTableCells)
|
||
// should use this wrapper so the per-region fan-out is bounded.
|
||
func (p *Parser) inferOCRRecognize(ctx context.Context, doc pdf.DocAnalyzer, cropped image.Image) ([]pdf.OCRText, error) {
|
||
if doc == nil || !doc.Health() {
|
||
return nil, nil
|
||
}
|
||
var texts []pdf.OCRText
|
||
err := p.limiters().withSlot(ctx, func() error {
|
||
t, callErr := doc.OCRRecognize(ctx, cropped)
|
||
if callErr != nil {
|
||
return callErr
|
||
}
|
||
texts = t
|
||
return nil
|
||
})
|
||
return texts, err
|
||
}
|