Skip to content

Commit 1ed2fc2

Browse files
authored
refactor(ingestion): use worker-driven task pull dispatcher (#19314)
1 parent 2c1af18 commit 1ed2fc2

31 files changed

Lines changed: 2230 additions & 1412 deletions

cmd/ragflow_server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ func runIngestor(ctx context.Context, cancel context.CancelFunc, args *serverArg
593593
// Search and UpsertDoc can embed queries/summaries automatically.
594594
nav.SetNavService(nlp.NewNavService(service.NewNavEmbedder(service.NewModelProviderService(), "")))
595595
// Memory extraction runs on the Ingestor's shared NATS consumer + worker
596-
// pool (task_type="memory" dispatched by processMessage -> executeMemoryTask),
596+
// pool (task_type="memory" dispatched by handleAndExecute -> executeMemoryTask),
597597
// so there is no longer a dedicated Redis memory consumer to start.
598598
ingestor.SetMemoryMessageService(service.NewMemoryMessageService(service.NewMemoryService()))
599599

internal/admin/handler.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package admin
1818

1919
import (
20+
"context"
2021
"encoding/json"
2122
"errors"
2223
"fmt"
@@ -923,7 +924,7 @@ func (h *Handler) PublishMessageToQueue(c *gin.Context) {
923924
}
924925

925926
msgQueueEngine := engine.GetMessageQueueEngine()
926-
err = msgQueueEngine.PublishTask("tasks.RAGFLOW", taskMessageStr)
927+
err = msgQueueEngine.PublishTask(common.TaskSubject, taskMessageStr)
927928
if err != nil {
928929
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
929930
return
@@ -933,7 +934,7 @@ func (h *Handler) PublishMessageToQueue(c *gin.Context) {
933934
}
934935

935936
type PullMessageFromQueueRequest struct {
936-
MessageCount int `json:"message_count" binding:"required"`
937+
MessageCount int `json:"message_count" binding:"required,gt=0"`
937938
AckPolicy string `json:"ack_policy" binding:"required"`
938939
}
939940

@@ -943,14 +944,20 @@ func (h *Handler) PullMessageFromQueue(c *gin.Context) {
943944
common.ErrorWithCode(c, common.CodeBadRequest, fmt.Sprintf("Message count error: %s", err.Error()))
944945
return
945946
}
947+
if req.MessageCount > common.MaxManualPullMessages {
948+
common.ErrorWithCode(c, common.CodeBadRequest,
949+
fmt.Sprintf("message count must be between 1 and %d", common.MaxManualPullMessages))
950+
return
951+
}
946952

947953
msgQueueEngine := engine.GetMessageQueueEngine()
948-
err := msgQueueEngine.InitConsumer("tasks.RAGFLOW")
949-
if err != nil {
954+
if err := msgQueueEngine.InitConsumer(common.TaskSubject); err != nil {
950955
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
951956
return
952957
}
953-
messages, err := msgQueueEngine.GetMessages(req.MessageCount)
958+
pullCtx, cancel := context.WithTimeout(c.Request.Context(), time.Second)
959+
defer cancel()
960+
messages, err := msgQueueEngine.PullMessages(pullCtx, req.MessageCount)
954961
if err != nil {
955962
common.ErrorWithCode(c, common.CodeBadRequest, err.Error())
956963
return
@@ -978,6 +985,7 @@ func (h *Handler) PullMessageFromQueue(c *gin.Context) {
978985
"id": taskMessage.TaskID,
979986
"type": taskMessage.TaskType,
980987
}
988+
err = message.Nack()
981989
if err == nil {
982990
resultMessage["nack"] = "true"
983991
} else {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//
2+
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//
16+
17+
package admin
18+
19+
import (
20+
"bytes"
21+
"encoding/json"
22+
"fmt"
23+
"net/http"
24+
"net/http/httptest"
25+
"testing"
26+
27+
"ragflow/internal/common"
28+
"ragflow/internal/engine"
29+
"ragflow/internal/ingestion/testutil"
30+
31+
"github.com/gin-gonic/gin"
32+
)
33+
34+
// TestPullMessageFromQueueInitializesSharedConsumer preserves the admin
35+
// endpoint's ability to pull messages before the ingestor starts. The endpoint
36+
// must initialize the existing durable consumer rather than creating a
37+
// dispatcher-specific consumer.
38+
func TestPullMessageFromQueueInitializesSharedConsumer(t *testing.T) {
39+
gin.SetMode(gin.TestMode)
40+
queue := testutil.SetupNatsEngine(t)
41+
previousQueue := engine.GetMessageQueueEngine()
42+
engine.SetMessageQueueEngine(queue)
43+
t.Cleanup(func() { engine.SetMessageQueueEngine(previousQueue) })
44+
45+
payload, err := json.Marshal(common.TaskMessage{
46+
TaskID: "admin-pull-before-ingestor",
47+
TaskType: common.TaskTypeIngestionTask,
48+
})
49+
if err != nil {
50+
t.Fatalf("marshal task: %v", err)
51+
}
52+
if err := queue.PublishTask(common.TaskSubject, payload); err != nil {
53+
t.Fatalf("publish task: %v", err)
54+
}
55+
56+
recorder := httptest.NewRecorder()
57+
ctx, _ := gin.CreateTestContext(recorder)
58+
ctx.Request = httptest.NewRequest(
59+
http.MethodPost,
60+
"/",
61+
bytes.NewBufferString(`{"message_count":1,"ack_policy":"ACK"}`),
62+
)
63+
ctx.Request.Header.Set("Content-Type", "application/json")
64+
65+
(&Handler{}).PullMessageFromQueue(ctx)
66+
67+
var response struct {
68+
Code int `json:"code"`
69+
Data []struct {
70+
ID string `json:"id"`
71+
Ack string `json:"ack"`
72+
} `json:"data"`
73+
}
74+
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
75+
t.Fatalf("decode response: %v", err)
76+
}
77+
if response.Code != int(common.CodeSuccess) {
78+
t.Fatalf("response code = %d, want %d; body = %s", response.Code, common.CodeSuccess, recorder.Body.String())
79+
}
80+
if len(response.Data) != 1 || response.Data[0].ID != "admin-pull-before-ingestor" || response.Data[0].Ack != "true" {
81+
t.Fatalf("pull response = %+v, want acked admin-pull-before-ingestor", response.Data)
82+
}
83+
}
84+
85+
func TestPullMessageFromQueueRejectsOutOfRangeMessageCount(t *testing.T) {
86+
gin.SetMode(gin.TestMode)
87+
for _, testCase := range []struct {
88+
name string
89+
messageCount int
90+
}{
91+
{name: "negative", messageCount: -1},
92+
{name: "zero", messageCount: 0},
93+
{name: "above limit", messageCount: 101},
94+
} {
95+
t.Run(testCase.name, func(t *testing.T) {
96+
recorder := httptest.NewRecorder()
97+
ctx, _ := gin.CreateTestContext(recorder)
98+
ctx.Request = httptest.NewRequest(
99+
http.MethodPost,
100+
"/",
101+
bytes.NewBufferString(fmt.Sprintf(`{"message_count":%d,"ack_policy":"ACK"}`, testCase.messageCount)),
102+
)
103+
ctx.Request.Header.Set("Content-Type", "application/json")
104+
105+
(&Handler{}).PullMessageFromQueue(ctx)
106+
107+
var response struct {
108+
Code int `json:"code"`
109+
}
110+
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
111+
t.Fatalf("decode response: %v", err)
112+
}
113+
if response.Code != int(common.CodeBadRequest) {
114+
t.Fatalf("response code = %d, want %d; body = %s", response.Code, common.CodeBadRequest, recorder.Body.String())
115+
}
116+
})
117+
}
118+
}

internal/cli/admin_parser.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2714,8 +2714,8 @@ func (p *Parser) parseMessageQueueCommand() (*Command, error) {
27142714
p.nextToken() // consume NUMBER
27152715
}
27162716

2717-
if messageCount <= 0 || messageCount > 100 {
2718-
return nil, fmt.Errorf("message count cannot be less than 0 or greater than 100")
2717+
if messageCount <= 0 || messageCount > common.MaxManualPullMessages {
2718+
return nil, fmt.Errorf("message count must be between 1 and %d", common.MaxManualPullMessages)
27192719
}
27202720

27212721
cmd = NewCommand("user_pull_message_command")

internal/common/task.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,17 @@ const (
2424
// symbol so the routing contract cannot diverge (mirrors the RAGFLOW_TASKS
2525
// JetStream subject in internal/engine/nats).
2626
TaskSubject = "tasks.RAGFLOW"
27+
// MaxManualPullMessages is the largest task batch the administrative queue
28+
// pull endpoint accepts.
29+
MaxManualPullMessages = 100
2730

2831
TaskTypeIngestionTask = "ingestion_task"
2932
TaskTypeIngestionTest = "ingestion_test"
3033
// TaskTypeSyncer is the NATS wake-up message type for datasource sync_logs tasks.
3134
TaskTypeSyncer = "syncer"
3235
// TaskTypeMemory is the async memory-extraction task type. Memory tasks
3336
// share the tasks.RAGFLOW subject and the Ingestor's consumer + worker
34-
// pool with ingestion tasks; processMessage dispatches them by TaskType.
37+
// pool with ingestion tasks; handleAndExecute dispatches them by TaskType.
3538
// The memory-specific payload (message_dict/memory_id/source_id) is
3639
// carried in TaskMessage.Payload.
3740
TaskTypeMemory = "memory"

internal/engine/engine.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ type MessageQueue interface {
113113
Type() string
114114
InitConsumer(subject string) error
115115
PublishTask(subject string, payload []byte) error
116-
GetMessages(messageCount int) ([]common.TaskHandle, error)
116+
PullMessages(ctx context.Context, messageCount int) ([]common.TaskHandle, error)
117+
PullMessage(ctx context.Context) (common.TaskHandle, error)
117118
ListMessages(messageType string, pending bool) ([]map[string]string, error)
118119
ShowMessageQueue() (map[string]string, error)
119120
CheckStatus() string

internal/engine/global.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ func InitMessageQueue() error {
111111
switch messageQueueType {
112112
case "nats":
113113
natsConfig := globalConfig.GetNATSConfig()
114-
messageQueueEngine = nats.NewNatsEngine(natsConfig.Host, natsConfig.Port)
114+
messageQueueEngine = nats.NewNatsEngine(
115+
natsConfig.Host,
116+
natsConfig.Port,
117+
)
115118
err := messageQueueEngine.Init()
116119
if err != nil {
117120
return err

internal/engine/nats/nats.go

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func (n *NatsEngine) PublishTask(subject string, payload []byte) error {
121121
// Duplicate delivery is instead made safe at the consumer level:
122122
// StartRunning's CREATED/SCHEDULED→RUNNING CAS plus the in-process claim guard
123123
// prevent a second copy from executing while the first owner is active (see
124-
// Ingestor.processMessage).
124+
// Ingestor.handleAndExecute).
125125
ack, err := n.jetStream.Publish(ctx, subject, payload)
126126
if err != nil {
127127
return err
@@ -256,22 +256,54 @@ func (n *NatsEngine) InitConsumer(subject string) error {
256256
}
257257
return nil
258258
}
259-
func (n *NatsEngine) GetMessages(messageCount int) ([]common.TaskHandle, error) {
259+
260+
// PullMessages fetches up to messageCount messages before ctx expires.
261+
func (n *NatsEngine) PullMessages(ctx context.Context, messageCount int) ([]common.TaskHandle, error) {
262+
if messageCount < 1 || messageCount > common.MaxManualPullMessages {
263+
return nil, fmt.Errorf("message count must be between 1 and %d", common.MaxManualPullMessages)
264+
}
260265
if n.consumer == nil {
261266
return nil, errors.New("NATS consumer is nil, engine not properly initialized")
262267
}
268+
if _, ok := ctx.Deadline(); !ok {
269+
return nil, errors.New("pull messages context must have a deadline")
270+
}
263271

264-
resultMessages := make([]common.TaskHandle, 0)
265-
messages, err := n.consumer.Fetch(messageCount, jetstream.FetchMaxWait(1*time.Second))
272+
resultMessages := make([]common.TaskHandle, 0, messageCount)
273+
messages, err := n.consumer.Fetch(messageCount, jetstream.FetchContext(ctx))
266274
if err != nil {
267275
return nil, fmt.Errorf("failed to fetch messages: %w", err)
268276
}
269-
for msg := range messages.Messages() {
270-
resultMessages = append(resultMessages, NewNatsMessageHandle(msg))
277+
for message := range messages.Messages() {
278+
resultMessages = append(resultMessages, NewNatsMessageHandle(message))
279+
}
280+
if batchErr := messages.Error(); batchErr != nil {
281+
if errors.Is(batchErr, context.DeadlineExceeded) {
282+
return resultMessages, nil
283+
}
284+
for _, message := range resultMessages {
285+
if nackErr := message.Nack(); nackErr != nil {
286+
common.Error("nack message after failed pull", nackErr)
287+
}
288+
}
289+
return nil, fmt.Errorf("failed to fetch messages: %w", batchErr)
271290
}
272291
return resultMessages, nil
273292
}
274293

294+
// PullMessage returns one task handle from PullMessages. A nil handle
295+
// with a nil error means the pull expired without an available task.
296+
func (n *NatsEngine) PullMessage(ctx context.Context) (common.TaskHandle, error) {
297+
messages, err := n.PullMessages(ctx, 1)
298+
if err != nil {
299+
return nil, err
300+
}
301+
if len(messages) == 0 {
302+
return nil, nil
303+
}
304+
return messages[0], nil
305+
}
306+
275307
func (n *NatsEngine) CheckStatus() string {
276308
if n.nc == nil {
277309
return "NATS connection is nil, engine not properly initialized"

internal/engine/nats/nats_uninitialized_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ func TestUninitializedEngineMethodsReturnErrors(t *testing.T) {
1919
t.Fatalf("PublishTask on uninitialized engine: err = %v, want 'not properly initialized'", err)
2020
}
2121

22-
if _, err := e.GetMessages(1); err == nil || !strings.Contains(err.Error(), "not properly initialized") {
23-
t.Fatalf("GetMessages on uninitialized engine: err = %v, want 'not properly initialized'", err)
22+
if _, err := e.PullMessages(t.Context(), 1); err == nil || !strings.Contains(err.Error(), "not properly initialized") {
23+
t.Fatalf("PullMessages on uninitialized engine: err = %v, want 'not properly initialized'", err)
2424
}
2525

2626
if _, err := e.ShowMessageQueue(); err == nil || !strings.Contains(err.Error(), "not properly initialized") {

0 commit comments

Comments
 (0)