Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,6 @@ jobs:

- name: Build server
run: go build ./cmd/server

- name: Build worker
run: go build ./cmd/worker
36 changes: 35 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"

"github.com/CeruleanFlow/cerulean/internal/dao"
"github.com/CeruleanFlow/cerulean/internal/queue"

"github.com/CeruleanFlow/cerulean/internal/api"
"github.com/CeruleanFlow/cerulean/internal/config"
Expand Down Expand Up @@ -35,8 +36,24 @@ func main() {
taskManager := task.NewMemoryManager()
documentParser := docparser.NewPDFTextParser()
searchBackend, err := buildSearchBackend(cfg)
if err != nil {
log.Fatal(err)
}
jobQueue, err := buildJobQueue(cfg)
if err != nil {
log.Fatal(err)
}

if closer, ok := jobQueue.(interface{ Close() error }); ok {
defer func() {
if err := closer.Close(); err != nil {
log.Printf("job queue close error: %v\n", err)
}
}()
}

ragService := rag.NewService(paperRepo, searchBackend)
ingestService := ingest.NewService(paperRepo, chunkRepo, objectStore, taskManager, searchBackend, documentParser)
ingestService := ingest.NewService(paperRepo, chunkRepo, objectStore, taskManager, searchBackend, documentParser, jobQueue)

router := api.NewRouter(api.RouterOptions{
Config: cfg,
Expand Down Expand Up @@ -113,3 +130,20 @@ func buildSearchBackend(cfg config.Config) (search.Backend, error) {
return nil, fmt.Errorf("unsupported CERULEAN_SEARCH_DRIVER=%q; supported: local, elastic", cfg.SearchDriver)
}
}

func buildJobQueue(cfg config.Config) (queue.Queue, error) {
switch strings.ToLower(cfg.QueueDriver) {
case "", "redis":
return queue.NewRedisStreamQueue(context.Background(), queue.RedisStreamConfig{
Addr: cfg.RedisAddr,
Password: cfg.RedisPassword,
DB: cfg.RedisDB,
Stream: cfg.QueueStream,
Group: cfg.QueueGroup,
Consumer: cfg.QueueConsumer,
})

default:
return nil, fmt.Errorf("unsupported CERULEAN_QUEUE_DRIVER=%q; supported: redis", cfg.QueueDriver)
}
}
155 changes: 155 additions & 0 deletions cmd/worker/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package main
Comment thread
coderabbitai[bot] marked this conversation as resolved.

import (
"context"
"fmt"
"log"
"os"
"os/signal"
"strings"
"syscall"
"time"

"github.com/CeruleanFlow/cerulean/internal/config"
"github.com/CeruleanFlow/cerulean/internal/dao"
"github.com/CeruleanFlow/cerulean/internal/executor"
"github.com/CeruleanFlow/cerulean/internal/ingest"
docparser "github.com/CeruleanFlow/cerulean/internal/parser"
"github.com/CeruleanFlow/cerulean/internal/pipeline"
"github.com/CeruleanFlow/cerulean/internal/queue"
"github.com/CeruleanFlow/cerulean/internal/search"
"github.com/CeruleanFlow/cerulean/internal/storage"
"github.com/CeruleanFlow/cerulean/internal/task"
)

func main() {
cfg := config.Load()

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()

q, err := queue.NewRedisStreamQueue(ctx, queue.RedisStreamConfig{
Addr: cfg.RedisAddr,
Password: cfg.RedisPassword,
DB: cfg.RedisDB,
Stream: cfg.QueueStream,
Group: cfg.QueueGroup,
Consumer: cfg.QueueConsumer,
})
if err != nil {
log.Fatalf("create redis queue: %v", err)
}
defer func() {
if err := q.Close(); err != nil {
log.Printf("close redis queue: %v", err)
}
}()

database, err := dao.NewMySQLDatabase(cfg.MySQLDSN)
if err != nil {
log.Fatalf("connect mysql: %v", err)
}

objectStore, err := buildObjectStorage(cfg)
if err != nil {
log.Fatalf("create object storage: %v", err)
}

searchBackend, err := buildSearchBackend(cfg)
if err != nil {
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.


documentParser := docparser.NewPDFTextParser()

ingestService := ingest.NewService(
database.Papers,
database.Chunks,
objectStore,
taskManager,
searchBackend,
documentParser,
nil,
)

registry := executor.NewRegistry()

if err := registry.Register(queue.JobTypePaperIngest, pipeline.NewPaperIngestHandler(ingestService)); err != nil {
log.Fatalf("register paper ingest handler: %v", err)
}

if err := registry.Register(queue.JobTypePaperReindex, pipeline.NewPaperReindexHandler(ingestService)); err != nil {
log.Fatalf("register paper reindex handler: %v", err)
}

worker, err := executor.NewWorker(q, registry, executor.WorkerOptions{
BatchSize: cfg.WorkerBatchSize,
BlockMillis: 5000,
JobTimeout: 30 * time.Minute,
Concurrency: cfg.WorkerConcurrency,
})
if err != nil {
log.Fatalf("create executor worker: %v", err)
}

log.Printf(
"Cerulean worker started: redis=%s stream=%s group=%s consumer=%s batch_size=%d concurrency=%d",
cfg.RedisAddr,
cfg.QueueStream,
cfg.QueueGroup,
cfg.QueueConsumer,
cfg.WorkerBatchSize,
cfg.WorkerConcurrency,
)

if err := worker.Run(ctx); err != nil && ctx.Err() == nil {
log.Fatalf("worker failed: %v", err)
}
}

func buildObjectStorage(cfg config.Config) (storage.ObjectStorage, error) {
switch strings.ToLower(cfg.StorageDriver) {
case "", "local":
return storage.NewLocalObjectStorage(cfg.LocalStorageDir)

case "minio":
useSSL := strings.EqualFold(cfg.MinIOUseSSL, "true")
return storage.NewMinIOObjectStorage(context.Background(), storage.MinIOConfig{
Endpoint: cfg.MinIOEndpoint,
AccessKey: cfg.MinIOAccessKey,
SecretKey: cfg.MinIOSecretKey,
Bucket: cfg.MinIOBucket,
UseSSL: useSSL,
})

default:
return nil, fmt.Errorf("unsupported CERULEAN_STORAGE_DRIVER=%q; supported: local, minio", cfg.StorageDriver)
}
}

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)
}
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ module github.com/CeruleanFlow/cerulean
go 1.26.0

require (
github.com/Haruko386/Celestial v0.0.0-20260628114458-d82cbffef886
github.com/gin-gonic/gin v1.12.0
github.com/joho/godotenv v1.5.1
github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728
github.com/minio/minio-go/v7 v7.2.1
github.com/redis/go-redis/v9 v9.21.0
gorm.io/datatypes v1.2.7
gorm.io/driver/mysql v1.6.0
gorm.io/gorm v1.31.2
Expand Down Expand Up @@ -51,6 +53,7 @@ require (
github.com/ugorji/go/codec v1.3.1 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Haruko386/Celestial v0.0.0-20260628114458-d82cbffef886 h1:o9fH5b0Rn8cwO7sRxf2Gnop4y9DIyve8skHzYEdO2D4=
github.com/Haruko386/Celestial v0.0.0-20260628114458-d82cbffef886/go.mod h1:Dv2Zzto5bivYyYxYOAVPNumTAG5amWkB6nh9M0J1zQg=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
Expand Down Expand Up @@ -102,6 +108,8 @@ github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/redis/go-redis/v9 v9.21.0 h1:FPBE4hhbAke+TLmcY3WkpbDffJEomdqPn3HYiqAtL9E=
github.com/redis/go-redis/v9 v9.21.0/go.mod h1:v/M13XI1PVCDcm01VtPFOADfZtHf8YW3baQf57KlIkA=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
Expand Down Expand Up @@ -129,6 +137,8 @@ github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
Expand Down
11 changes: 3 additions & 8 deletions internal/api/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -267,17 +267,12 @@ func (h *Handler) ReindexPaper(c *gin.Context) {
return
}

optCtx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
defer cancel()

if err := h.ingest.ReindexPaper(optCtx, id); err != nil {
job, err := h.ingest.StartPaperReindex(c.Request.Context(), id)
if err != nil {
writeError(c, http.StatusInternalServerError, err)
return
}
c.JSON(http.StatusOK, gin.H{
"paper_id": id,
"status": "reindexed",
})
c.JSON(http.StatusAccepted, job)
}

func (h *Handler) GetTask(c *gin.Context) {
Expand Down
37 changes: 37 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"os"
"strconv"

"github.com/joho/godotenv"
_ "github.com/joho/godotenv"
Expand Down Expand Up @@ -35,6 +36,18 @@ type Config struct {

LLMBaseURL string
LLMModel string

RedisAddr string
RedisPassword string
RedisDB int

QueueDriver string
QueueStream string
QueueGroup string
QueueConsumer string

WorkerConcurrency int
WorkerBatchSize int
}

func Load() Config {
Expand Down Expand Up @@ -70,6 +83,18 @@ func Load() Config {

LLMBaseURL: env("CERULEAN_LLM_BASE_URL", ""),
LLMModel: env("CERULEAN_LLM_MODEL", ""),

RedisAddr: env("CERULEAN_REDIS_ADDR", "127.0.0.1:6379"),
RedisPassword: env("CERULEAN_REDIS_PASSWORD", ""),
RedisDB: envInt("CERULEAN_REDIS_DB", 0),

QueueDriver: env("CERULEAN_QUEUE_DRIVER", "redis"),
QueueStream: env("CERULEAN_QUEUE_STREAM", "cerulean_tasks"),
QueueConsumer: env("CERULEAN_QUEUE_CONSUMER", "worker_local_1"),
QueueGroup: env("CERULEAN_QUEUE_GROUP", "cerulean_workers"),

WorkerBatchSize: envInt("CERULEAN_WORKER_BATCH_SIZE", 4),
WorkerConcurrency: envInt("CERULEAN_WORKER_CONCURRENCY", 16),
}
}

Expand All @@ -79,3 +104,15 @@ func env(key, fallback string) string {
}
return fallback
}

func envInt(key string, fallback int) int {
value := os.Getenv(key)
if value == "" {
return fallback
}
i, err := strconv.Atoi(value)
if err != nil {
return fallback
}
return i
}
17 changes: 17 additions & 0 deletions internal/executor/executor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package executor

import (
"context"

"github.com/CeruleanFlow/cerulean/internal/queue"
)

type Handler interface {
Handle(ctx context.Context, job queue.Job) error
}

type HandlerFunc func(ctx context.Context, job queue.Job) error

func (f HandlerFunc) Handle(ctx context.Context, job queue.Job) error {
return f(ctx, job)
}
Loading
Loading