Files
Enterprise-Knowledge-Base/internal/infrastructure/docparser/grpc_parser.go
T
wizardchen 7b1bb1054f feat(docreader): speed up scanned-PDF parsing, stream image results, isolate heavy async queues
Large scanned PDFs (hundreds of pages) were slow and fragile end-to-end.
This change addresses the parse, transport, and task-scheduling layers:

docreader (parse + transport):
- Parallelize per-page scanned rendering across processes (forkserver/fork),
  with serial fallback. ~4-7x faster on large scanned PDFs; pdfium is not
  thread-safe so we fan out across processes. Configurable via
  DOCREADER_PDF_RENDER_PARALLELISM.
- Add server-streaming ReadStream RPC: emit one meta frame then one frame per
  image, so documents with many page images are no longer capped by the unary
  gRPC message-size limit (a 874-page PDF produced ~193MiB of images, far over
  the 50MB cap) and memory is bounded on both ends. Unary Read is kept for
  backward compatibility; the Go production reader switches to ReadStream.

VLM:
- Make the VLM HTTP timeout configurable (VLM_HTTP_TIMEOUT_SECONDS) and raise
  the default 90s -> 180s so dense scanned-page OCR does not time out with
  "context deadline exceeded".

Async task queues:
- Isolate high-volume, model-heavy fan-out tasks into dedicated asynq queues so
  a single large document cannot saturate the shared worker pool and block
  user-facing document parsing:
    image:multimodal  -> "multimodal"
    chunk:extract     -> "graph"
    question:generation -> "question"
- Register the new queues in the server weight map and the cancel inspector's
  scanned-queue set (so cancelling a knowledge still purges its pending tasks).
2026-06-03 12:29:13 +08:00

202 lines
5.0 KiB
Go

package docparser
import (
"context"
"fmt"
"io"
"os"
"strconv"
"sync"
"time"
docclient "github.com/Tencent/WeKnora/docreader/client"
"github.com/Tencent/WeKnora/docreader/proto"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/types"
"google.golang.org/grpc"
"google.golang.org/grpc/resolver"
)
func getMaxMessageSize() int {
if sizeStr := os.Getenv("MAX_FILE_SIZE_MB"); sizeStr != "" {
if size, err := strconv.Atoi(sizeStr); err == nil && size > 0 {
return size * 1024 * 1024
}
}
return 50 * 1024 * 1024
}
// GRPCDocumentReader implements DocumentReader over gRPC.
type GRPCDocumentReader struct {
mu sync.RWMutex
conn *grpc.ClientConn
client proto.DocReaderClient
addr string
}
func NewGRPCDocumentReader(addr string) (*GRPCDocumentReader, error) {
p := &GRPCDocumentReader{}
if addr != "" {
if err := p.connect(addr); err != nil {
return nil, err
}
}
return p, nil
}
func (p *GRPCDocumentReader) connect(addr string) error {
authConfig := docclient.LoadAuthConfigFromEnv()
opts, err := authConfig.BuildDialOptions(getMaxMessageSize())
if err != nil {
return fmt.Errorf("failed to build docreader dial options: %w", err)
}
if authConfig.TLSEnabled {
logger.Infof(context.Background(), "TLS enabled for docreader gRPC client")
}
if authConfig.AuthToken != "" {
logger.Infof(context.Background(),
"Token authentication enabled for docreader gRPC client (TLS=%v)",
authConfig.TLSEnabled,
)
}
resolver.SetDefaultScheme("dns")
start := time.Now()
conn, err := grpc.Dial("dns:///"+addr, opts...)
if err != nil {
return fmt.Errorf("failed to connect to docreader: %w", err)
}
logger.Infof(context.Background(), "Connected to docreader in %v", time.Since(start))
p.conn = conn
p.client = proto.NewDocReaderClient(conn)
p.addr = addr
return nil
}
func (p *GRPCDocumentReader) Reconnect(addr string) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.conn != nil {
_ = p.conn.Close()
p.conn = nil
p.client = nil
p.addr = ""
}
return p.connect(addr)
}
func (p *GRPCDocumentReader) IsConnected() bool {
p.mu.RLock()
defer p.mu.RUnlock()
return p.conn != nil
}
func (p *GRPCDocumentReader) Close() error {
p.mu.Lock()
defer p.mu.Unlock()
if p.conn != nil {
return p.conn.Close()
}
return nil
}
var errNotConnected = fmt.Errorf("docreader service not connected")
func (p *GRPCDocumentReader) Read(ctx context.Context, req *types.ReadRequest) (*types.ReadResult, error) {
p.mu.RLock()
client := p.client
p.mu.RUnlock()
if client == nil {
return nil, errNotConnected
}
protoReq := &proto.ReadRequest{
FileContent: req.FileContent,
FileName: req.FileName,
FileType: req.FileType,
Url: req.URL,
Title: req.Title,
RequestId: req.RequestID,
Config: &proto.ReadConfig{
ParserEngine: req.ParserEngine,
ParserEngineOverrides: req.ParserEngineOverrides,
},
}
// Use the streaming RPC so documents with many page images (large scanned
// PDFs) are not capped by the unary message-size limit. The meta frame
// arrives first, followed by one frame per image.
stream, err := client.ReadStream(ctx, protoReq)
if err != nil {
return nil, fmt.Errorf("gRPC ReadStream failed: %w", err)
}
result := &types.ReadResult{}
gotMeta := false
for {
frame, recvErr := stream.Recv()
if recvErr == io.EOF {
break
}
if recvErr != nil {
return nil, fmt.Errorf("gRPC ReadStream recv failed: %w", recvErr)
}
if meta := frame.GetMeta(); meta != nil {
gotMeta = true
result.MarkdownContent = meta.GetMarkdownContent()
result.ImageDirPath = meta.GetImageDirPath()
result.Metadata = meta.GetMetadata()
result.Error = meta.GetError()
if n := meta.GetImageCount(); n > 0 {
result.ImageRefs = make([]types.ImageRef, 0, n)
}
continue
}
if img := frame.GetImage(); img != nil {
result.ImageRefs = append(result.ImageRefs, types.ImageRef{
Filename: img.GetFilename(),
OriginalRef: img.GetOriginalRef(),
MimeType: img.GetMimeType(),
StorageKey: img.GetStorageKey(),
ImageData: img.GetImageData(),
})
}
}
if !gotMeta {
return nil, fmt.Errorf("gRPC ReadStream returned no metadata frame")
}
return result, nil
}
func (p *GRPCDocumentReader) ListEngines(ctx context.Context, overrides map[string]string) ([]types.ParserEngineInfo, error) {
p.mu.RLock()
client := p.client
p.mu.RUnlock()
if client == nil {
return nil, errNotConnected
}
resp, err := client.ListEngines(ctx, &proto.ListEnginesRequest{ConfigOverrides: overrides})
if err != nil {
return nil, fmt.Errorf("gRPC ListEngines failed: %w", err)
}
result := make([]types.ParserEngineInfo, 0, len(resp.GetEngines()))
for _, e := range resp.GetEngines() {
result = append(result, types.ParserEngineInfo{
Name: e.GetName(),
Description: e.GetDescription(),
FileTypes: e.GetFileTypes(),
Available: e.GetAvailable(),
UnavailableReason: e.GetUnavailableReason(),
})
}
return result, nil
}