This repository was archived by the owner on Jun 23, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Introduces Background Worker for Async Processing #44
Merged
Merged
Changes from all commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
6966723
feat: Make CORS origins configurable via environment variables
FinnTheHero ab3d4dd
feat: Unify CORS origin configuration with single DOMAIN env var
FinnTheHero 6843b5f
feat: Expose Content-Type header in CORS configuration
FinnTheHero 0161d75
Adds Procfile for process management
FinnTheHero 4133c1a
Introduces dedicated worker process
FinnTheHero 0e0b2d6
Refactors environment variable handling
FinnTheHero 408fdcd
Chore: Enhance CORS configuration for cookie handling
FinnTheHero 579595c
Refactor: Remove placeholder worker exit
FinnTheHero 06bbf96
Feature: Integrate Riverqueue for asynchronous task processing
FinnTheHero 0abe34d
Enhances HTML content cleaning
FinnTheHero 0033862
feat: Define ProcessEPUBArgs for worker jobs
FinnTheHero 122b77f
refactor: Export River client initialization function
FinnTheHero a4672ee
feat: Enqueue EPUB processing as a background job
FinnTheHero de6397b
feat: Implement EPUB processing worker
FinnTheHero 9fe0a52
Refines go.mod dependencies
FinnTheHero bae1c75
Refactors EPUB worker and queue management
FinnTheHero fdcdcba
Improves chapter batch upload reliability
FinnTheHero 0afc08f
Updates README with detailed API documentation
FinnTheHero 61b2fcf
Refines chapter cursor pagination logic
FinnTheHero 7f66c4e
Implements worker graceful shutdown
FinnTheHero c4e2192
Allows wildcard CORS for development
FinnTheHero d7f0bfe
Refines wildcard domain assignment logic
FinnTheHero b745e1f
Update README.md
FinnTheHero a0326ed
Adds graceful API server shutdown
FinnTheHero e4de72b
Merge pull request #43 from FinnTheHero/heroku-postgres
FinnTheHero a1760b2
Uses compiled binaries for processes
FinnTheHero 619e568
Merge branch 'master' into development
FinnTheHero e9a5c58
refactor: Delegate chapter ID/timestamp generation in batch upload
FinnTheHero 5475abe
feat: Remove batch chapter upload endpoint
FinnTheHero 80c6f1f
refactor: Categorize manage routes with comments
FinnTheHero 3bc63b4
Removed commented code
FinnTheHero 189ddba
Implements singleton River client initialization
FinnTheHero c53ee27
Unifies River queue client setup
FinnTheHero File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| web: ./bin/web | ||
| worker: ./bin/worker |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,20 +1,54 @@ | ||
| # Codex Backend | ||
|
|
||
| Backend for Codex - novel reader app. | ||
| Backend for Codex - novel reading platform. | ||
|
|
||
| # Details | ||
| ## Details | ||
|
|
||
| Codex-Backend is built in `GoLang`, using `Gin` for server and `AWS-dynamoDB` for database. | ||
| Codex-Backend is built in `GoLang`, using `Gin` for server and ~AWS-dynamoDB~ firestore (moving to Heroku Postgres) for database. | ||
|
|
||
| It is deployed on `Heroku` (thats why the code is in api directory). | ||
|
|
||
| ### Endpoints | ||
| air config is outdated and not recommended. use [Run](Run guide instead) | ||
|
|
||
| This will be updated later (the version before was already outdated) | ||
| ## Run | ||
| run server: | ||
|
|
||
| ### Run | ||
| ```bash | ||
| go run api/cmd/web/main.go | ||
| ``` | ||
|
|
||
| - run server: | ||
| ```bash | ||
| go run api/cmd/codex/main.go | ||
| ``` | ||
| ```bash | ||
| go run api/cmd/worker/main.go | ||
| ``` | ||
|
|
||
| Both are needed | ||
|
|
||
| ## Endpoints | ||
|
|
||
| 3 Groups of endpoints: Client, Manage and User. | ||
|
|
||
| - Client is responsible for basic GET requests. | ||
| - Manage is responsible for Upload/Modification operations. | ||
| - User is responsible for user authentication, authorization and Registration (Delete is not yet implemented). | ||
|
|
||
| ### Client: base path followed by request path | ||
| - `/all` - Get all novels | ||
| - `/:novel` - Get a novel by id | ||
| - `/:novel/:chapter` - Get chapter from novel using both ids | ||
| - `/:novel/all` - Get all chapters from novel using id | ||
| - `/:novel/chapter` - Get cursor paginated chapters from novel using id | ||
|
|
||
| Options: limit (max 100), cursor (chapter index (integer)) and sort ("asc" || "desc"). | ||
|
|
||
| Defaults: limit=100, cursor=0, sort="desc" | ||
|
|
||
| ### Manage: `/manage` followed by request path | ||
| - `/upload` - Upload novel | ||
| - `/:novel` - Update novel | ||
| - `/:novel/:chapter` - Update chapter | ||
|
|
||
| ### User: `/user` followed by request path | ||
| - `/validate` - Validate user token | ||
| - `/login` - Login user | ||
| - `/register` - Register user | ||
| - `/logout` - Logout user |
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| cmn "Codex-Backend/api/internal/common" | ||
| firestore_server "Codex-Backend/api/internal/interfaces/rest" | ||
| "os" | ||
|
|
||
| _ "github.com/heroku/x/hmetrics/onload" | ||
| ) | ||
|
|
||
| func init() { | ||
| if mode := os.Getenv("GIN_MODE"); mode == "debug" { | ||
| cmn.LoadEnvVariables() | ||
| } | ||
| } | ||
|
|
||
| func main() { | ||
| firestore_server.Server() | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| cmn "Codex-Backend/api/internal/common" | ||
| queue "Codex-Backend/api/internal/common/river" | ||
| "context" | ||
| "log" | ||
| "os" | ||
| "os/signal" | ||
| "syscall" | ||
| "time" | ||
| ) | ||
|
|
||
| func init() { | ||
| if mode := os.Getenv("GIN_MODE"); mode == "debug" { | ||
| cmn.LoadEnvVariables() | ||
| } | ||
| } | ||
|
|
||
| func main() { | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| defer cancel() | ||
|
|
||
| riverClient := queue.GetRiverClient(ctx) | ||
| if err := riverClient.Start(ctx); err != nil { | ||
| log.Fatal("Failed to start River client:", err) | ||
| } | ||
|
|
||
| log.Println("River worker started successfully, waiting for jobs...") | ||
|
|
||
| sigChan := make(chan os.Signal, 1) | ||
| signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) | ||
|
|
||
| <-sigChan | ||
| log.Println("Shutdown signal received, starting graceful shutdown...") | ||
|
|
||
| cancel() | ||
|
|
||
| shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second) | ||
| defer shutdownCancel() | ||
|
|
||
| if err := riverClient.Stop(shutdownCtx); err != nil { | ||
| log.Printf("Error during River client shutdown: %v", err) | ||
| } else { | ||
| log.Println("River worker stopped gracefully") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,21 +2,25 @@ package common | |
|
|
||
| import ( | ||
| "errors" | ||
| "log" | ||
| "net/http" | ||
| "os" | ||
|
|
||
| "github.com/joho/godotenv" | ||
| ) | ||
|
|
||
| func LoadEnvVariables() error { | ||
| return godotenv.Load(".env") | ||
| func LoadEnvVariables() { | ||
| err := godotenv.Load(".env") | ||
| if err != nil { | ||
| log.Fatal(&Error{Err: errors.New("Failed to load environment variables"), Status: http.StatusInternalServerError}) | ||
| } | ||
| } | ||
|
|
||
| func GetEnvVariable(v string) (string, error) { | ||
| func GetEnvVariable(v string) string { | ||
| env_variable := os.Getenv(v) | ||
| if env_variable == "" { | ||
| return "", &Error{Err: errors.New("Environmental Variable " + v + " Not Found"), Status: http.StatusNotFound} | ||
| log.Fatal(&Error{Err: errors.New("Environmental Variable " + v + " Not Found"), Status: http.StatusNotFound}) | ||
|
||
| } | ||
|
|
||
| return env_variable, nil | ||
| return env_variable | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package queue | ||
|
|
||
| import ( | ||
| "Codex-Backend/api/internal/usecases/worker" | ||
| "context" | ||
| "log" | ||
| "log/slog" | ||
| "os" | ||
| "sync" | ||
|
|
||
| "github.com/jackc/pgx/v5" | ||
| "github.com/jackc/pgx/v5/pgxpool" | ||
| "github.com/riverqueue/river" | ||
| "github.com/riverqueue/river/riverdriver/riverpgxv5" | ||
| ) | ||
|
|
||
| var ( | ||
| riverClient *river.Client[pgx.Tx] | ||
| riverOnce sync.Once | ||
| ) | ||
|
|
||
| func InitializeRiverClient(ctx context.Context, workers *river.Workers) *river.Client[pgx.Tx] { | ||
| dbPool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL")) | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
|
|
||
| logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ | ||
| Level: slog.LevelInfo, | ||
| })) | ||
|
|
||
| riverClient, err := river.NewClient(riverpgxv5.New(dbPool), &river.Config{ | ||
| Logger: logger, | ||
| Queues: map[string]river.QueueConfig{ | ||
| river.QueueDefault: {MaxWorkers: 10}, | ||
| }, | ||
| MaxAttempts: 3, | ||
| Workers: workers, | ||
| }) | ||
| if err != nil { | ||
| panic(err) | ||
| } | ||
|
|
||
| return riverClient | ||
| } | ||
|
|
||
| func GetRiverClient(ctx context.Context) *river.Client[pgx.Tx] { | ||
| riverOnce.Do(func() { | ||
| log.Println("Initializing River client...") | ||
|
|
||
| workers := river.NewWorkers() | ||
| river.AddWorker(workers, &worker.EPUBWorker{}) | ||
|
|
||
| riverClient = InitializeRiverClient(ctx, workers) | ||
|
|
||
| log.Println("River client initialized successfully") | ||
| }) | ||
|
|
||
| return riverClient | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -21,30 +21,16 @@ func (c *Client) CursorPagination(options domain.CursorOptions, ctx context.Cont | |||||
| limit := min(max(options.Limit, 1), 100) | ||||||
|
|
||||||
| snapshots := []*firestore.DocumentSnapshot{} | ||||||
| var err error | ||||||
|
|
||||||
| if options.Cursor == 0 { | ||||||
| snaps, err := query.Limit(1).Documents(ctx).GetAll() | ||||||
| if err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
|
|
||||||
| if len(snaps) == 0 { | ||||||
| return nil, &cmn.Error{ | ||||||
| Err: fmt.Errorf("Firestore Client Error - Get Paginated Chapters - No Chapters Found for Novel: %s", options.NovelID), | ||||||
| Status: http.StatusNotFound, | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| snapshots, err = query.StartAt(snaps[0]).Limit(limit + 1).Documents(ctx).GetAll() | ||||||
| if err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
| snapshots, err = query.Limit(limit + 1).Documents(ctx).GetAll() | ||||||
| } else { | ||||||
| var err error | ||||||
| snapshots, err = query.StartAt(options.Cursor).Limit(limit + 1).Documents(ctx).GetAll() | ||||||
| if err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| if err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
|
|
||||||
| if len(snapshots) == 0 { | ||||||
|
|
@@ -54,22 +40,10 @@ func (c *Client) CursorPagination(options domain.CursorOptions, ctx context.Cont | |||||
| } | ||||||
| } | ||||||
|
|
||||||
| nextCursor := 0 | ||||||
| if len(snapshots) > limit { | ||||||
| var chapter domain.Chapter | ||||||
| if err := snapshots[len(snapshots)-1].DataTo(&chapter); err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
| nextCursor = chapter.Index | ||||||
| } | ||||||
|
|
||||||
| snapLen := len(snapshots) - 1 | ||||||
| if snapLen <= 0 { | ||||||
| snapLen++ | ||||||
| } | ||||||
| actualLimit := min(len(snapshots), limit) | ||||||
| chapters := make([]domain.FrontendChapter, 0, actualLimit) | ||||||
|
|
||||||
| chapters := []domain.FrontendChapter{} | ||||||
| for _, snapshot := range snapshots[:snapLen] { | ||||||
| for _, snapshot := range snapshots[:actualLimit] { | ||||||
| var chapter domain.Chapter | ||||||
| if err := snapshot.DataTo(&chapter); err != nil { | ||||||
| return nil, err | ||||||
|
|
@@ -82,6 +56,15 @@ func (c *Client) CursorPagination(options domain.CursorOptions, ctx context.Cont | |||||
| }) | ||||||
| } | ||||||
|
|
||||||
| nextCursor := 0 | ||||||
| if len(snapshots) > limit { | ||||||
| var lastChapter domain.Chapter | ||||||
| if err := snapshots[limit].DataTo(&lastChapter); err != nil { | ||||||
| return nil, err | ||||||
| } | ||||||
| nextCursor = lastChapter.Index | ||||||
| } | ||||||
|
|
||||||
| return &domain.CursorResponse{ | ||||||
| Chapters: chapters, | ||||||
| NextCursor: nextCursor, | ||||||
|
|
@@ -95,7 +78,7 @@ func (c *Client) BatchUploadChapters(novelId string, chapters []domain.Chapter, | |||||
| for i := 0; i < len(chapters); i += chunkSize { | ||||||
| subset := chapters[i:min(i+chunkSize, len(chapters))] | ||||||
|
|
||||||
| batchCtx, cancel := context.WithTimeout(ctx, 300*time.Second) | ||||||
| batchCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) | ||||||
|
||||||
| batchCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) | |
| batchCtx, cancel := context.WithTimeout(ctx, 10*time.Minute) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
log.Fatal expects a string or values that can be formatted, but &Error{} is a struct pointer. Use log.Fatal(err) or log.Fatalf with proper formatting.