move job queue

This commit is contained in:
Erik Winter 2024-03-09 13:08:25 +01:00
parent 4d207eed8b
commit 72e91d27ed
8 changed files with 66 additions and 400 deletions

View File

@ -1,323 +0,0 @@
package client
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
)
type EMDB struct {
baseURL string
apiKey string
c *http.Client
}
func NewEMDB(baseURL string, apiKey string) *EMDB {
return &EMDB{
baseURL: baseURL,
apiKey: apiKey,
c: &http.Client{},
}
}
func (e *EMDB) GetMovies() ([]moviestore.Movie, error) {
url := fmt.Sprintf("%s/movie", e.baseURL)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", e.apiKey)
resp, err := e.c.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
var movies []moviestore.Movie
if err := json.Unmarshal(body, &movies); err != nil {
return nil, err
}
return movies, nil
}
func (e *EMDB) GetMovie(id string) (moviestore.Movie, error) {
url := fmt.Sprintf("%s/movie/%s", e.baseURL, id)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return moviestore.Movie{}, err
}
req.Header.Add("Authorization", e.apiKey)
resp, err := e.c.Do(req)
if err != nil {
return moviestore.Movie{}, err
}
if resp.StatusCode != http.StatusOK {
return moviestore.Movie{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
var movie moviestore.Movie
if err := json.Unmarshal(body, &movie); err != nil {
return moviestore.Movie{}, err
}
return movie, nil
}
func (e *EMDB) CreateMovie(m moviestore.Movie) (moviestore.Movie, error) {
body, err := json.Marshal(m)
if err != nil {
return moviestore.Movie{}, err
}
url := fmt.Sprintf("%s/movie", e.baseURL)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return moviestore.Movie{}, err
}
req.Header.Add("Authorization", e.apiKey)
resp, err := e.c.Do(req)
if err != nil {
return moviestore.Movie{}, err
}
if resp.StatusCode != http.StatusOK {
return moviestore.Movie{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
newBody, err := io.ReadAll(resp.Body)
if err != nil {
return moviestore.Movie{}, err
}
defer resp.Body.Close()
var newMovie moviestore.Movie
if err := json.Unmarshal(newBody, &newMovie); err != nil {
return moviestore.Movie{}, err
}
return newMovie, nil
}
func (e *EMDB) UpdateMovie(m moviestore.Movie) (moviestore.Movie, error) {
body, err := json.Marshal(m)
if err != nil {
return moviestore.Movie{}, err
}
url := fmt.Sprintf("%s/movie/%s", e.baseURL, m.ID)
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(body))
if err != nil {
return moviestore.Movie{}, err
}
req.Header.Add("Authorization", e.apiKey)
resp, err := e.c.Do(req)
if err != nil {
return moviestore.Movie{}, err
}
if resp.StatusCode != http.StatusOK {
return moviestore.Movie{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
newBody, err := io.ReadAll(resp.Body)
if err != nil {
return moviestore.Movie{}, err
}
defer resp.Body.Close()
var newMovie moviestore.Movie
if err := json.Unmarshal(newBody, &newMovie); err != nil {
return moviestore.Movie{}, err
}
return newMovie, nil
}
func (e *EMDB) GetReviews(movieID string) ([]moviestore.Review, error) {
url := fmt.Sprintf("%s/movie/%s/review", e.baseURL, movieID)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Add("Authorization", e.apiKey)
resp, err := e.c.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
var reviews []moviestore.Review
if err := json.Unmarshal(body, &reviews); err != nil {
return nil, err
}
return reviews, nil
}
func (e *EMDB) GetReview(id string) (moviestore.Review, error) {
url := fmt.Sprintf("%s/review/%s", e.baseURL, id)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return moviestore.Review{}, err
}
req.Header.Add("Authorization", e.apiKey)
resp, err := e.c.Do(req)
if err != nil {
return moviestore.Review{}, err
}
if resp.StatusCode != http.StatusOK {
return moviestore.Review{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
var review moviestore.Review
if err := json.Unmarshal(body, &review); err != nil {
return moviestore.Review{}, err
}
return review, nil
}
func (e *EMDB) GetNextUnratedReview() (moviestore.Review, error) {
url := fmt.Sprintf("%s/review/unrated/next", e.baseURL)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return moviestore.Review{}, err
}
req.Header.Add("Authorization", e.apiKey)
resp, err := e.c.Do(req)
if err != nil {
return moviestore.Review{}, err
}
if resp.StatusCode != http.StatusOK {
return moviestore.Review{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
var review moviestore.Review
if err := json.Unmarshal(body, &review); err != nil {
return moviestore.Review{}, err
}
return review, nil
}
func (e *EMDB) UpdateReview(review moviestore.Review) error {
body, err := json.Marshal(review)
if err != nil {
return err
}
url := fmt.Sprintf("%s/review/%s", e.baseURL, review.ID)
req, err := http.NewRequest(http.MethodPut, url, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Add("Authorization", e.apiKey)
resp, err := e.c.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
return nil
}
func (e *EMDB) CreateJob(movieID, action string) error {
j := struct {
MovieID string
Action string
}{
MovieID: movieID,
Action: action,
}
body, err := json.Marshal(j)
if err != nil {
return err
}
url := fmt.Sprintf("%s/job", e.baseURL)
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return err
}
req.Header.Add("Authorization", e.apiKey)
resp, err := e.c.Do(req)
if err != nil {
return err
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
return nil
}
func (e *EMDB) GetNextAIJob() (moviestore.Job, error) {
url := fmt.Sprintf("%s/job/next-ai", e.baseURL)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return moviestore.Job{}, err
}
req.Header.Add("Authorization", e.apiKey)
resp, err := e.c.Do(req)
if err != nil {
return moviestore.Job{}, err
}
if resp.StatusCode != http.StatusOK {
return moviestore.Job{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
defer resp.Body.Close()
var j moviestore.Job
if err := json.Unmarshal(body, &j); err != nil {
return moviestore.Job{}, err
}
return j, nil
}

View File

@ -8,7 +8,6 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
"code.ewintr.nl/emdb/job" "code.ewintr.nl/emdb/job"
) )
@ -47,7 +46,7 @@ func (jobAPI *JobAPI) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (jobAPI *JobAPI) Add(w http.ResponseWriter, r *http.Request) { func (jobAPI *JobAPI) Add(w http.ResponseWriter, r *http.Request) {
logger := jobAPI.logger.With("method", "add") logger := jobAPI.logger.With("method", "add")
var j moviestore.Job var j job.Job
if err := json.NewDecoder(r.Body).Decode(&j); err != nil { if err := json.NewDecoder(r.Body).Decode(&j); err != nil {
Error(w, http.StatusBadRequest, "could not decode job", err, logger) Error(w, http.StatusBadRequest, "could not decode job", err, logger)
return return
@ -82,7 +81,7 @@ func (jobAPI *JobAPI) List(w http.ResponseWriter, r *http.Request) {
func (jobAPI *JobAPI) NextAI(w http.ResponseWriter, r *http.Request) { func (jobAPI *JobAPI) NextAI(w http.ResponseWriter, r *http.Request) {
logger := jobAPI.logger.With("method", "nextai") logger := jobAPI.logger.With("method", "nextai")
j, err := jobAPI.jq.Next(moviestore.TypeAI) j, err := jobAPI.jq.Next(job.TypeAI)
switch { switch {
case errors.Is(err, sql.ErrNoRows): case errors.Is(err, sql.ErrNoRows):
logger.Info("no ai jobs found") logger.Info("no ai jobs found")

View File

@ -26,7 +26,6 @@ func NewMovieAPI(apis APIIndex, repo *moviestore.MovieRepository, jq *job.JobQue
return &MovieAPI{ return &MovieAPI{
apis: apis, apis: apis,
repo: repo, repo: repo,
jq: jq,
logger: logger.With("api", "movie"), logger: logger.With("api", "movie"),
} }
} }

View File

@ -1,51 +0,0 @@
package moviestore
import (
"slices"
"time"
)
type JobStatus string
type JobType string
const (
interval = 20 * time.Second
TypeSimple JobType = "simple"
TypeAI JobType = "ai"
ActionRefreshIMDBReviews = "refresh-imdb-reviews"
ActionRefreshAllIMDBReviews = "refresh-all-imdb-reviews"
ActionFindTitles = "find-titles"
ActionFindAllTitles = "find-all-titles"
)
var (
SimpleActions = []string{
ActionRefreshIMDBReviews,
ActionRefreshAllIMDBReviews, // just creates a job for each movie
ActionFindAllTitles, // just creates a job for each review
}
AIActions = []string{
ActionFindTitles,
}
ValidActions = append(SimpleActions, AIActions...)
)
type Job struct {
ID int
ActionID string
Action string
Status JobStatus
Created time.Time
Updated time.Time
}
func Valid(action string) bool {
if slices.Contains(ValidActions, action) {
return true
}
return false
}

View File

@ -1,7 +1,51 @@
package job package job
import "time" import (
"slices"
"time"
)
type JobStatus string
type JobType string
const ( const (
interval = 20 * time.Second interval = 20 * time.Second
TypeSimple JobType = "simple"
TypeAI JobType = "ai"
ActionRefreshIMDBReviews = "refresh-imdb-reviews"
ActionRefreshAllIMDBReviews = "refresh-all-imdb-reviews"
ActionFindTitles = "find-titles"
ActionFindAllTitles = "find-all-titles"
) )
var (
SimpleActions = []string{
ActionRefreshIMDBReviews,
ActionRefreshAllIMDBReviews, // just creates a job for each movie
ActionFindAllTitles, // just creates a job for each review
}
AIActions = []string{
ActionFindTitles,
}
ValidActions = append(SimpleActions, AIActions...)
)
type Job struct {
ID int
ActionID string
Action string
Status JobStatus
Created time.Time
Updated time.Time
}
func Valid(action string) bool {
if slices.Contains(ValidActions, action) {
return true
}
return false
}

View File

@ -8,7 +8,6 @@ import (
"strings" "strings"
"time" "time"
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
"code.ewintr.nl/emdb/storage" "code.ewintr.nl/emdb/storage"
) )
@ -47,7 +46,7 @@ WHERE status = 'doing'
} }
func (jq *JobQueue) Add(movieID, action string) error { func (jq *JobQueue) Add(movieID, action string) error {
if !moviestore.Valid(action) { if !Valid(action) {
return errors.New("invalid action") return errors.New("invalid action")
} }
@ -58,12 +57,12 @@ VALUES ($1, $2, 'todo');`, movieID, action)
return err return err
} }
func (jq *JobQueue) Next(t moviestore.JobType) (moviestore.Job, error) { func (jq *JobQueue) Next(t JobType) (Job, error) {
logger := jq.logger.With("method", "next") logger := jq.logger.With("method", "next")
actions := moviestore.SimpleActions actions := SimpleActions
if t == moviestore.TypeAI { if t == TypeAI {
actions = moviestore.AIActions actions = AIActions
} }
actionsStr := fmt.Sprintf("('%s')", strings.Join(actions, "', '")) actionsStr := fmt.Sprintf("('%s')", strings.Join(actions, "', '"))
query := fmt.Sprintf(` query := fmt.Sprintf(`
@ -74,13 +73,13 @@ WHERE status='todo'
ORDER BY id ASC ORDER BY id ASC
LIMIT 1;`, actionsStr) LIMIT 1;`, actionsStr)
row := jq.db.QueryRow(query) row := jq.db.QueryRow(query)
var job moviestore.Job var job Job
err := row.Scan(&job.ID, &job.ActionID, &job.Action) err := row.Scan(&job.ID, &job.ActionID, &job.Action)
if err != nil { if err != nil {
if !errors.Is(err, sql.ErrNoRows) { if !errors.Is(err, sql.ErrNoRows) {
logger.Error("could not fetch next job", "error", err) logger.Error("could not fetch next job", "error", err)
} }
return moviestore.Job{}, err return Job{}, err
} }
logger.Info("found a job", "id", job.ID) logger.Info("found a job", "id", job.ID)
@ -89,7 +88,7 @@ UPDATE job_queue
SET status='doing' SET status='doing'
WHERE id=$1;`, job.ID); err != nil { WHERE id=$1;`, job.ID); err != nil {
logger.Error("could not set job to doing", "error") logger.Error("could not set job to doing", "error")
return moviestore.Job{}, err return Job{}, err
} }
return job, nil return job, nil
@ -116,7 +115,7 @@ WHERE id=$1;`, id); err != nil {
return return
} }
func (jq *JobQueue) List() ([]moviestore.Job, error) { func (jq *JobQueue) List() ([]Job, error) {
rows, err := jq.db.Query(` rows, err := jq.db.Query(`
SELECT id, action_id, action, status, created_at, updated_at SELECT id, action_id, action, status, created_at, updated_at
FROM job_queue FROM job_queue
@ -126,9 +125,9 @@ ORDER BY id DESC;`)
} }
defer rows.Close() defer rows.Close()
var jobs []moviestore.Job var jobs []Job
for rows.Next() { for rows.Next() {
var j moviestore.Job var j Job
if err := rows.Scan(&j.ID, &j.ActionID, &j.Action, &j.Status, &j.Created, &j.Updated); err != nil { if err := rows.Scan(&j.ID, &j.ActionID, &j.Action, &j.Status, &j.Created, &j.Updated); err != nil {
return nil, err return nil, err
} }

View File

@ -33,7 +33,7 @@ func (w *Worker) Run() {
logger.Info("starting worker") logger.Info("starting worker")
for { for {
time.Sleep(interval) time.Sleep(interval)
j, err := w.jq.Next(moviestore.TypeSimple) j, err := w.jq.Next(TypeSimple)
switch { switch {
case errors.Is(err, sql.ErrNoRows): case errors.Is(err, sql.ErrNoRows):
logger.Info("no simple jobs found") logger.Info("no simple jobs found")
@ -45,11 +45,11 @@ func (w *Worker) Run() {
logger.Info("got a new job", "jobID", j.ID, "movieID", j.ActionID, "action", j.Action) logger.Info("got a new job", "jobID", j.ID, "movieID", j.ActionID, "action", j.Action)
switch j.Action { switch j.Action {
case moviestore.ActionRefreshIMDBReviews: case ActionRefreshIMDBReviews:
w.RefreshReviews(j.ID, j.ActionID) w.RefreshReviews(j.ID, j.ActionID)
case moviestore.ActionRefreshAllIMDBReviews: case ActionRefreshAllIMDBReviews:
w.RefreshAllReviews(j.ID) w.RefreshAllReviews(j.ID)
case moviestore.ActionFindAllTitles: case ActionFindAllTitles:
w.FindAllTitles(j.ID) w.FindAllTitles(j.ID)
default: default:
logger.Error("unknown job action", "action", j.Action) logger.Error("unknown job action", "action", j.Action)
@ -68,7 +68,7 @@ func (w *Worker) RefreshAllReviews(jobID int) {
for _, m := range movies { for _, m := range movies {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
if err := w.jq.Add(m.ID, moviestore.ActionRefreshIMDBReviews); err != nil { if err := w.jq.Add(m.ID, ActionRefreshIMDBReviews); err != nil {
logger.Error("could not add job", "error", err) logger.Error("could not add job", "error", err)
return return
} }
@ -90,7 +90,7 @@ func (w *Worker) FindAllTitles(jobID int) {
for _, r := range reviews { for _, r := range reviews {
time.Sleep(1 * time.Second) time.Sleep(1 * time.Second)
if err := w.jq.Add(r.ID, moviestore.ActionFindTitles); err != nil { if err := w.jq.Add(r.ID, ActionFindTitles); err != nil {
logger.Error("could not add job", "error", err) logger.Error("could not add job", "error", err)
w.jq.MarkFailed(jobID) w.jq.MarkFailed(jobID)
return return
@ -130,7 +130,7 @@ func (w *Worker) RefreshReviews(jobID int, movieID string) {
w.jq.MarkFailed(jobID) w.jq.MarkFailed(jobID)
return return
} }
if err := w.jq.Add(review.ID, moviestore.ActionFindTitles); err != nil { if err := w.jq.Add(review.ID, ActionFindTitles); err != nil {
logger.Error("could not add job", "error", err) logger.Error("could not add job", "error", err)
w.jq.MarkFailed(jobID) w.jq.MarkFailed(jobID)
return return

View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"code.ewintr.nl/emdb/client" "code.ewintr.nl/emdb/client"
"code.ewintr.nl/emdb/cmd/api-service/moviestore"
"code.ewintr.nl/emdb/job" "code.ewintr.nl/emdb/job"
"code.ewintr.nl/emdb/storage" "code.ewintr.nl/emdb/storage"
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
@ -134,7 +133,7 @@ func (m *tabTMDB) ImportMovieCmd(movie Movie) tea.Cmd {
if err := m.movieRepo.Store(movie.m); err != nil { if err := m.movieRepo.Store(movie.m); err != nil {
return err return err
} }
if err := m.jobQueue.Add(movie.m.ID, string(moviestore.ActionRefreshIMDBReviews)); err != nil { if err := m.jobQueue.Add(movie.m.ID, string(job.ActionRefreshIMDBReviews)); err != nil {
return err return err
} }