Skip to content

Feature: add redis stream queue - #13

Merged
Haruko386 merged 4 commits into
CeruleanFlow:mainfrom
Haruko386:main
Jul 10, 2026
Merged

Feature: add redis stream queue#13
Haruko386 merged 4 commits into
CeruleanFlow:mainfrom
Haruko386:main

Conversation

@Haruko386

@Haruko386 Haruko386 commented Jul 7, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features
    • Added a background worker binary to process paper ingest and reindex tasks asynchronously.
    • Introduced a job executor/handler registry and queue-backed worker processing (batching, concurrency, per-job timeouts).
    • Added a Redis Stream–backed job queue and new ingest pipeline handlers that validate inputs before running.
    • Updated the reindex API to return 202 Accepted with the enqueued job.
  • Bug Fixes
    • Improved Elasticsearch index creation precondition checks and error reporting.
  • Chores
    • Updated CI to build the worker binary.

@Haruko386 Haruko386 added this to the v1.0 milestone Jul 7, 2026
@Haruko386 Haruko386 self-assigned this Jul 7, 2026
@Haruko386 Haruko386 added the 🧩feature New feature for the project label Jul 7, 2026
@Haruko386 Haruko386 linked an issue Jul 7, 2026 that may be closed by this pull request
11 tasks
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 47bd8119-799e-47e4-a189-a28f488533b5

📥 Commits

Reviewing files that changed from the base of the PR and between 2a79788 and cf6afc7.

📒 Files selected for processing (4)
  • internal/api/handler.go
  • internal/pipeline/paper_ingest.go
  • internal/pipeline/paper_reindex.go
  • internal/queue/redis_stream.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/api/handler.go
  • internal/queue/redis_stream.go

📝 Walkthrough

Walkthrough

This PR replaces in-process ingest execution with Redis Stream jobs, adds executor and worker runtime behavior, wires queue configuration into server and worker startup, registers pipeline handlers, changes reindex requests to return accepted tasks, and updates Elasticsearch index handling and CI worker builds.

Changes

Queued ingest worker pipeline

Layer / File(s) Summary
Queue contracts and Redis transport
internal/queue/*, go.mod
Defines job/message contracts and implements Redis Stream enqueue, batch dequeue, acknowledgement, group initialization, and queue closure.
Configuration and process wiring
internal/config/config.go, cmd/server/main.go, cmd/worker/main.go
Loads Redis, queue, and worker settings; wires Redis queues into the server and worker; and adds signal-aware worker startup and shutdown.
Executor registry and worker loop
internal/executor/*
Adds handler registration and validation, batch polling, concurrent dispatch, per-job timeouts, and Ack/Nack handling.
Queued ingest lifecycle
internal/ingest/service.go, internal/api/handler.go, internal/pipeline/*
Enqueues ingest and reindex tasks, adds worker-facing processing methods and task failure handling, returns accepted reindex tasks, and registers pipeline handlers.
Search validation and build coverage
internal/search/elastic.go, .github/workflows/ci.yml
Adds Elasticsearch request guards and status handling, corrects index creation path construction, and builds the worker binary in CI.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant API
  participant IngestService
  participant RedisStreamQueue
  participant Worker
  participant PipelineHandler
  API->>IngestService: StartPaperIngest or StartPaperReindex
  IngestService->>RedisStreamQueue: Enqueue(queue.Job)
  API-->>API: Return accepted task
  Worker->>RedisStreamQueue: DequeueBatch(...)
  RedisStreamQueue-->>Worker: queue.Message
  Worker->>PipelineHandler: Handle(ctx, job)
  PipelineHandler->>IngestService: ProcessPaperIngest or ProcessPaperReindex
  IngestService-->>PipelineHandler: processing result
  Worker->>RedisStreamQueue: Ack or Nack(message)
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a Redis Streams-backed queue and related worker pipeline.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@Dimon0000000
Dimon0000000 marked this pull request as draft July 9, 2026 08:30
@Dimon0000000
Dimon0000000 marked this pull request as ready for review July 9, 2026 08:30

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
internal/queue/redis_stream.go (2)

87-102: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Stream is never trimmed — unbounded growth risk.

Enqueue calls XAdd with no MaxLen/Approx (or TTL/MINID) cap, so cerulean_tasks will grow without bound as jobs accumulate, consuming ever-increasing Redis memory over time.

♻️ Proposed fix
 	return q.client.XAdd(ctx, &redis.XAddArgs{
 		Stream: q.stream,
+		MaxLen: 100_000,
+		Approx: true,
 		Values: map[string]any{
 			"payload":  string(payload),
 			"type":     job.Type,
 			"task_id":  job.TaskID,
 			"paper_id": job.PaperID,
 		},
 	}).Err()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/queue/redis_stream.go` around lines 87 - 102, The Redis stream in
RedisStreamQueue.Enqueue is being appended to without any retention cap, which
can cause unbounded growth. Update the q.client.XAdd call in Enqueue to enforce
stream trimming using Redis max-length or MINID retention (prefer approximate
trimming if acceptable), and make the limit configurable if needed so the stream
does not grow forever.

31-71: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Client is leaked if ensureGroup fails; no Close() exposed for graceful shutdown.

If ensureGroup fails (Line 66-68), the already-connected client is dropped without calling client.Close(), leaking the connection pool. More generally, RedisStreamQueue never exposes a way to close the underlying client at all, making graceful shutdown of the worker impossible.

♻️ Proposed fix
 	if err := q.ensureGroup(ctx); err != nil {
+		client.Close()
 		return nil, err
 	}

 	return q, nil
 }
+
+func (q *RedisStreamQueue) Close() error {
+	return q.client.Close()
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/queue/redis_stream.go` around lines 31 - 71, The Redis client
created in NewRedisStreamQueue is leaked when q.ensureGroup fails because the
connected client is returned from scope without being closed. Update
NewRedisStreamQueue to close client on any post-connect failure path, especially
after ensureGroup, and add a Close method on RedisStreamQueue that delegates to
the underlying redis.Client so callers can shut it down cleanly during worker
shutdown.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/worker/main.go`:
- Line 1: The worker entrypoint is missing from the cmd/worker package, so the
binary cannot build. Add a func main() to the main package in cmd/worker/main.go
and ensure it performs the worker startup path expected by the existing worker
code, using the main package entrypoint as the unique location to fix.

In `@internal/queue/redis_stream.go`:
- Around line 159-161: The RedisStreamQueue Nack implementation is a permanent
no-op, so failed messages are never requeued, retried, or dead-lettered. Update
Nack on RedisStreamQueue to either implement the intended retry/recovery path
(for example via XClaim/XAutoClaim handling in the worker flow) or, if this is
intentionally deferred, add an explicit TODO/documentation in Nack and the Queue
contract so callers know no retry semantics are provided yet.
- Around line 151-157: The Ack method in RedisStreamQueue is passing msg.RedisID
as both the consumer group and the message ID, so the pending entry is never
acknowledged for q.group. Update Ack to call XAck with q.stream, q.group, and
the message RedisID as the ID argument only, keeping the empty RedisID guard
intact.
- Around line 128-149: DequeueBatch in redis_stream.go is silently skipping
malformed Redis stream entries with missing payloads or bad JSON, leaving them
stuck in the consumer PEL. Update the loop that builds messages from
stream.Messages to explicitly handle these failures by logging the bad entry and
either acking it immediately or routing it through the queue’s dead-letter path
instead of continuing past it. Use the existing DequeueBatch flow and the
redisMsg.ID/job parsing logic to ensure malformed records cannot be stranded.
- Around line 104-119: The RedisStreamQueue.DequeueBatch timeout handling only
normalizes negative blockMillis, so a value of 0 still gets passed through to
XReadGroup as BLOCK 0 and blocks forever. Update the guard in DequeueBatch to
treat 0 the same as an unset timeout and fall back to the default 5000ms, while
keeping the existing max normalization and XReadGroup call unchanged.

---

Nitpick comments:
In `@internal/queue/redis_stream.go`:
- Around line 87-102: The Redis stream in RedisStreamQueue.Enqueue is being
appended to without any retention cap, which can cause unbounded growth. Update
the q.client.XAdd call in Enqueue to enforce stream trimming using Redis
max-length or MINID retention (prefer approximate trimming if acceptable), and
make the limit configurable if needed so the stream does not grow forever.
- Around line 31-71: The Redis client created in NewRedisStreamQueue is leaked
when q.ensureGroup fails because the connected client is returned from scope
without being closed. Update NewRedisStreamQueue to close client on any
post-connect failure path, especially after ensureGroup, and add a Close method
on RedisStreamQueue that delegates to the underlying redis.Client so callers can
shut it down cleanly during worker shutdown.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: dc3dd6e2-bad2-44b1-8e51-3a3dd00e98b6

📥 Commits

Reviewing files that changed from the base of the PR and between 9acfb83 and 30c0051.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (11)
  • cmd/worker/main.go
  • go.mod
  • internal/config/config.go
  • internal/executor/executor.go
  • internal/executor/registry.go
  • internal/executor/worker.go
  • internal/pipeline/paper_ingest.go
  • internal/pipeline/paper_reindex.go
  • internal/queue/job.go
  • internal/queue/queue.go
  • internal/queue/redis_stream.go

Comment thread cmd/worker/main.go
Comment thread internal/queue/redis_stream.go
Comment on lines +128 to +149
messages := make([]Message, 0)

for _, stream := range streams {
for _, redisMsg := range stream.Messages {
raw, ok := redisMsg.Values["payload"].(string)
if !ok {
continue
}

var job Job
if err := json.Unmarshal([]byte(raw), &job); err != nil {
continue
}

messages = append(messages, Message{
RedisID: redisMsg.ID,
Job: job,
})
}
}
return messages, nil
}

@coderabbitai coderabbitai Bot Jul 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the target file and inspect the relevant functions.
ast-grep outline internal/queue/redis_stream.go --view expanded || true
wc -l internal/queue/redis_stream.go
sed -n '1,260p' internal/queue/redis_stream.go

# Search for acknowledgement / recovery / dead-letter behavior in the queue package.
rg -n "Ack|Nack|XAck|XClaim|XAUTOCLAIM|XPENDING|dead.?letter|DLQ|reclaim|pending" internal/queue -S

Repository: CeruleanFlow/Cerulean

Length of output: 4973


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the queue interface and all call sites of DequeueBatch/Ack/Nack.
sed -n '1,220p' internal/queue/queue.go
rg -n "DequeueBatch\\(|\\.Ack\\(|\\.Nack\\(" -S .

# Inspect Message definition for context.
rg -n "type Message struct|type Job struct" -S internal/queue
sed -n '1,220p' internal/queue/*.go 2>/dev/null | sed -n '/type Message struct/,/type /p'

Repository: CeruleanFlow/Cerulean

Length of output: 939


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search the whole repo for any Redis stream recovery / dead-letter handling.
rg -n "XAUTOCLAIM|XCLAIM|XPENDING|XDEL|dead.?letter|DLQ|reclaim|retry.*stream|pending forever" -S .

# Search for queue usage to understand whether callers can ever see malformed messages.
rg -n "\.DequeueBatch\(|queue\.Queue|NewRedisStreamQueue|Ack\(|Nack\(" -S .

Repository: CeruleanFlow/Cerulean

Length of output: 159


Malformed stream entries need an explicit escape path
DequeueBatch drops messages with a missing payload or bad JSON before returning them, and this queue has no reclaim/dead-letter path. Those entries stay in the consumer PEL and can’t be acked or nacked, so they remain stuck until handled explicitly. Consider acking-and-logging or routing them to a dead-letter flow instead of silently skipping them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/queue/redis_stream.go` around lines 128 - 149, DequeueBatch in
redis_stream.go is silently skipping malformed Redis stream entries with missing
payloads or bad JSON, leaving them stuck in the consumer PEL. Update the loop
that builds messages from stream.Messages to explicitly handle these failures by
logging the bad entry and either acking it immediately or routing it through the
queue’s dead-letter path instead of continuing past it. Use the existing
DequeueBatch flow and the redisMsg.ID/job parsing logic to ensure malformed
records cannot be stranded.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shall fix these later, we ll merge this PR

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Haruko386 Understood, thanks for confirming. Merging now with the fix deferred sounds reasonable — the malformed messages issue is an edge case unlikely to block core functionality.

Would you like me to open a follow-up GitHub issue to track this so it doesn't get lost? Just let me know and I'll create it with a link back to this PR and comment for reference.

ヾ(•ω•`)o

Comment thread internal/queue/redis_stream.go
Comment on lines +159 to +161
func (q *RedisStreamQueue) Nack(ctx context.Context, msg Message, reason error) error {
return nil
}

@coderabbitai coderabbitai Bot Jul 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Nack is a permanent no-op.

No requeue, retry-with-backoff, or dead-letter handling is implemented; failed jobs simply remain in the PEL forever with no automatic recovery path (e.g. via XClaim/XAutoClaim). Given the Queue interface commits to this contract, callers relying on Nack for retry semantics will silently get none.

If this is intentionally deferred to a later PR in the stack (e.g. the executor/worker layer), consider a TODO comment here to make that explicit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/queue/redis_stream.go` around lines 159 - 161, The RedisStreamQueue
Nack implementation is a permanent no-op, so failed messages are never requeued,
retried, or dead-lettered. Update Nack on RedisStreamQueue to either implement
the intended retry/recovery path (for example via XClaim/XAutoClaim handling in
the worker flow) or, if this is intentionally deferred, add an explicit
TODO/documentation in Nack and the Queue contract so callers know no retry
semantics are provided yet.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as last review

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Haruko386 Understood — deferring the Nack retry/dead-letter implementation to a later PR, consistent with the previous review, sounds reasonable given the scope here.

To keep this visible for future work, consider adding a short // TODO: comment directly above the Nack stub in internal/queue/redis_stream.go noting that requeue/backoff/dead-letter handling (e.g., via XClaim/XAutoClaim) is intentionally deferred.

Would you like me to open a follow-up GitHub issue to track implementing proper Nack semantics, so it doesn't get lost? Just let me know and I'll create it.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
internal/search/elastic.go (1)

104-119: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add focused tests for the new status branches.

Cover 200, 404, and unexpected statuses, including verification that unexpected responses preserve both status and body in the returned error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/search/elastic.go` around lines 104 - 119, 添加针对检查索引状态逻辑的聚焦测试,覆盖
StatusOK、StatusNotFound 和非预期状态分支;使用可控的 HTTP 响应验证 200/404
成功返回,并确认非预期响应的错误同时包含状态码和响应体内容。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/api/handler.go`:
- Around line 270-275: Update the ReindexPaper handler to call
h.ingest.StartPaperReindex instead of h.ingest.StartPaperIngest, preserving the
existing context, paper ID, error handling, and accepted response.

In `@internal/pipeline/paper_ingest.go`:
- Around line 22-37: In PaperIngestHandler.Handle, replace the
ProcessPaperReindex call with ProcessPaperIngest and pass the identifiers in the
target method’s required order: ctx, paperID, taskID. Preserve the existing
validation and trimmed values.

In `@internal/pipeline/paper_reindex.go`:
- Around line 23-33: Update the validation errors in the reindex handler: use a
message describing the missing ingest dependency for the h.ingest nil check, a
task ID-specific message for the empty taskID check, and retain a paper
ID-specific message for the empty paperID check. Locate these checks in the
function containing h.ingest, taskID, and paperID validation.
- Around line 22-37: PaperReindexHandler.Handle passes the identifiers to
ProcessPaperReindex in the wrong order. Update the call to match
ProcessPaperReindex’s signature by passing paperID before taskID, while
preserving the existing validation and context argument.

---

Nitpick comments:
In `@internal/search/elastic.go`:
- Around line 104-119: 添加针对检查索引状态逻辑的聚焦测试,覆盖 StatusOK、StatusNotFound
和非预期状态分支;使用可控的 HTTP 响应验证 200/404 成功返回,并确认非预期响应的错误同时包含状态码和响应体内容。
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b7044cff-a434-46a8-8c4b-7c614d12aef4

📥 Commits

Reviewing files that changed from the base of the PR and between 30c0051 and 9f30ce2.

📒 Files selected for processing (16)
  • .github/workflows/ci.yml
  • cmd/server/main.go
  • cmd/worker/main.go
  • internal/api/handler.go
  • internal/config/config.go
  • internal/executor/executor.go
  • internal/executor/registry.go
  • internal/executor/worker.go
  • internal/ingest/service.go
  • internal/pipeline/log_job.go
  • internal/pipeline/paper_ingest.go
  • internal/pipeline/paper_reindex.go
  • internal/queue/redis_stream.go
  • internal/search/elastic.go
  • server
  • worker
✅ Files skipped from review due to trivial changes (1)
  • internal/pipeline/log_job.go

Comment thread internal/api/handler.go Outdated
Comment on lines +22 to +37
func (h *PaperIngestHandler) Handle(ctx context.Context, job queue.Job) error {
if h.ingest == nil {
return fmt.Errorf("ingest service is nil")
}

taskID := strings.TrimSpace(job.TaskID)
paperID := strings.TrimSpace(job.PaperID)
if taskID == "" {
return fmt.Errorf("task id is empty")
}
if paperID == "" {
return fmt.Errorf("paper id is empty")
}

return h.ingest.ProcessPaperReindex(ctx, taskID, paperID)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Wrong method called with swapped arguments — breaks queued paper ingest entirely.

Two compounding bugs on line 36:

  1. ProcessPaperReindex is called instead of ProcessPaperIngest, so paper_ingest jobs never run runPDFTextIngest — they attempt a chunk reindex on a paper that has no chunks yet.
  2. Even ignoring (1), the args are passed as (ctx, taskID, paperID) while the target method's signature is (ctx context.Context, paperID, taskID string) — so the values land in swapped fields, causing s.tasks.Get(ctx, taskID) inside the callee to look up a task by the paper's ID and fail with "task not found" for every job.

This makes the queued ingest pipeline non-functional for all paper_ingest jobs.

🐛 Proposed fix
-	return h.ingest.ProcessPaperReindex(ctx, taskID, paperID)
+	return h.ingest.ProcessPaperIngest(ctx, paperID, taskID)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (h *PaperIngestHandler) Handle(ctx context.Context, job queue.Job) error {
if h.ingest == nil {
return fmt.Errorf("ingest service is nil")
}
taskID := strings.TrimSpace(job.TaskID)
paperID := strings.TrimSpace(job.PaperID)
if taskID == "" {
return fmt.Errorf("task id is empty")
}
if paperID == "" {
return fmt.Errorf("paper id is empty")
}
return h.ingest.ProcessPaperReindex(ctx, taskID, paperID)
}
func (h *PaperIngestHandler) Handle(ctx context.Context, job queue.Job) error {
if h.ingest == nil {
return fmt.Errorf("ingest service is nil")
}
taskID := strings.TrimSpace(job.TaskID)
paperID := strings.TrimSpace(job.PaperID)
if taskID == "" {
return fmt.Errorf("task id is empty")
}
if paperID == "" {
return fmt.Errorf("paper id is empty")
}
return h.ingest.ProcessPaperIngest(ctx, paperID, taskID)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/pipeline/paper_ingest.go` around lines 22 - 37, In
PaperIngestHandler.Handle, replace the ProcessPaperReindex call with
ProcessPaperIngest and pass the identifiers in the target method’s required
order: ctx, paperID, taskID. Preserve the existing validation and trimmed
values.

Comment thread internal/pipeline/paper_reindex.go
Comment thread internal/pipeline/paper_reindex.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
internal/executor/worker.go (1)

112-166: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: reuse a single dispatcher instead of allocating one per batch.

handleBatch builds a new celestial dispatcher (and its worker goroutines) on every dequeue cycle. Under steady load this recreates the pool repeatedly. Consider constructing the dispatcher once in NewWorker and reusing it across batches.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/executor/worker.go` around lines 112 - 166, Optionally avoid
recreating the celestial worker pool for every batch by constructing the
dispatcher once in NewWorker and storing it on Worker. Update handleBatch to
reuse that dispatcher while preserving the existing configuration and result
handling, and ensure its lifecycle is properly managed during worker shutdown.
cmd/worker/main.go (1)

132-155: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix the misleading search-driver error and drop the commented-out branch.

Two issues:

  • An empty SearchDriver falls through to default and errors out (unlike buildObjectStorage, which treats "" as local), yet the error message advertises local as supported.
  • Lines 134-135 are dead commented-out code.

Either restore a real default/local branch or correct the message to list only the actually-supported drivers.

♻️ Suggested cleanup
 func buildSearchBackend(cfg config.Config) (search.Backend, error) {
 	switch strings.ToLower(cfg.SearchDriver) {
-	//case "", "local":
-	//	return search.NewLocalBackend(), nil
-
 	case "elastic", "elasticsearch", "es":
 		backend, err := search.NewElasticBackend(context.Background(), search.ElasticConfig{
 			URL:      cfg.ElasticURL,
 			Index:    cfg.ElasticIndex,
 			Username: cfg.ElasticUsername,
 			Password: cfg.ElasticPassword,
 		})
 		if err != nil {
 			return nil, err
 		}
 		if backend == nil {
 			return nil, fmt.Errorf("elastic backend constructor returned nil")
 		}
 		return backend, nil
 
 	default:
-		return nil, fmt.Errorf("unsupported CERULEAN_SEARCH_DRIVER=%q; supported: local, elastic", cfg.SearchDriver)
+		return nil, fmt.Errorf("unsupported CERULEAN_SEARCH_DRIVER=%q; supported: elastic", cfg.SearchDriver)
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/worker/main.go` around lines 132 - 155, Remove the dead commented-out
local branch in buildSearchBackend and make its behavior and error message
consistent: either implement a real empty/local case or, if local is
unsupported, remove “local” from the advertised supported drivers and keep the
default error accurate.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/worker/main.go`:
- Line 63: Replace the process-local task.NewMemoryManager() in the worker with
a shared Redis- or database-backed task store, and configure cmd/server/main.go
to use the same backend and storage settings so worker updates are visible
through the API’s /tasks/:id endpoints.

In `@internal/executor/worker.go`:
- Around line 199-207: Failed jobs remain pending because Queue.Nack is
currently a no-op. Implement the failure handling flow in the queue
implementation, including retry/reclaim or dead-letter behavior and
removal/acknowledgment of the pending message, then update the worker’s Nack
path to use it consistently; reference the Nack method in
internal/queue/redis_stream.go and the worker error-handling block.

---

Nitpick comments:
In `@cmd/worker/main.go`:
- Around line 132-155: Remove the dead commented-out local branch in
buildSearchBackend and make its behavior and error message consistent: either
implement a real empty/local case or, if local is unsupported, remove “local”
from the advertised supported drivers and keep the default error accurate.

In `@internal/executor/worker.go`:
- Around line 112-166: Optionally avoid recreating the celestial worker pool for
every batch by constructing the dispatcher once in NewWorker and storing it on
Worker. Update handleBatch to reuse that dispatcher while preserving the
existing configuration and result handling, and ensure its lifecycle is properly
managed during worker shutdown.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d99dc8d6-d11c-4fe4-94f8-f4f93b719faa

📥 Commits

Reviewing files that changed from the base of the PR and between 9f30ce2 and 2a79788.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (3)
  • cmd/worker/main.go
  • go.mod
  • internal/executor/worker.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • go.mod

Comment thread cmd/worker/main.go
log.Fatalf("create search backend: %v", err)
}

taskManager := task.NewMemoryManager()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm server also uses an in-memory manager and whether task state is persisted anywhere shared.
rg -nP --type=go 'NewMemoryManager|task\.Manager|tasks\s' cmd internal/task internal/api internal/ingest -C2

Repository: CeruleanFlow/Cerulean

Length of output: 2339


🏁 Script executed:

#!/bin/bash
sed -n '1,220p' internal/task/task.go
printf '\n---\n'
sed -n '1,220p' internal/api/handler.go
printf '\n---\n'
sed -n '1,180p' cmd/server/main.go
printf '\n---\n'
sed -n '1,180p' cmd/worker/main.go

Repository: CeruleanFlow/Cerulean

Length of output: 14815


Use a shared task store

task.NewMemoryManager() is process-local, and cmd/server/main.go creates its own separate in-memory manager, so task updates recorded by the worker won’t be visible to the API’s /tasks/:id endpoints. Back this with Redis or the DB instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/worker/main.go` at line 63, Replace the process-local
task.NewMemoryManager() in the worker with a shared Redis- or database-backed
task store, and configure cmd/server/main.go to use the same backend and storage
settings so worker updates are visible through the API’s /tasks/:id endpoints.

Comment on lines +199 to +207
nackCtx, cancel := context.WithTimeout(context.Background(), w.jobTimeout)
defer cancel()

if nackErr := w.queue.Nack(nackCtx, msg, err); nackErr != nil {
log.Printf("nack job failed: redis_id=%s task_id=%s err=%v", msg.RedisID, msg.Job.TaskID, nackErr)
}

return result, err
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect Nack and any pending-entry reclaim/retry logic in the redis stream queue.
rg -nP --type=go 'func .*Nack|XAutoClaim|XClaim|XPending|dead.?letter' internal/queue -C3

Repository: CeruleanFlow/Cerulean

Length of output: 575


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== queue file outline =="
ast-grep outline internal/queue/redis_stream.go --view expanded || true

echo
echo "== worker file outline =="
ast-grep outline internal/executor/worker.go --view expanded || true

echo
echo "== search for reclaim/retry/pending handling across repo =="
rg -n --type=go 'XAutoClaim|XClaim|XPending|Pending|claim.*pending|dead.?letter|requeue|retry' internal -C 2 || true

echo
echo "== relevant queue methods =="
sed -n '1,260p' internal/queue/redis_stream.go

Repository: CeruleanFlow/Cerulean

Length of output: 5587


Failed jobs are stuck in the pending list

internal/queue/redis_stream.go makes Nack a no-op, so errors in internal/executor/worker.go leave messages pending forever with no retry, reclaim, or dead-letter path. Implement the intended failure flow before relying on Nack here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/executor/worker.go` around lines 199 - 207, Failed jobs remain
pending because Queue.Nack is currently a no-op. Implement the failure handling
flow in the queue implementation, including retry/reclaim or dead-letter
behavior and removal/acknowledgment of the pending message, then update the
worker’s Nack path to use it consistently; reference the Nack method in
internal/queue/redis_stream.go and the worker error-handling block.

@Haruko386 Haruko386 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixing

@Haruko386 Haruko386 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merging soon

Comment on lines +128 to +149
messages := make([]Message, 0)

for _, stream := range streams {
for _, redisMsg := range stream.Messages {
raw, ok := redisMsg.Values["payload"].(string)
if !ok {
continue
}

var job Job
if err := json.Unmarshal([]byte(raw), &job); err != nil {
continue
}

messages = append(messages, Message{
RedisID: redisMsg.ID,
Job: job,
})
}
}
return messages, nil
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shall fix these later, we ll merge this PR

Comment on lines +159 to +161
func (q *RedisStreamQueue) Nack(ctx context.Context, msg Message, reason error) error {
return nil
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as last review

@Haruko386
Haruko386 merged commit da5a668 into CeruleanFlow:main Jul 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🧩feature New feature for the project

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Road Map] v1.0 development is in progress

1 participant