Files
silo-server/internal/taskmanager/tasks/sync_ebook_metadata.go
T

191 lines
5.3 KiB
Go

package tasks
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/Silo-Server/silo-server/internal/ebooks"
"github.com/Silo-Server/silo-server/internal/taskmanager"
)
const ebookMetadataExecutionBudget = 4 * time.Minute
type ebookMetadataEnricher interface {
Run(ctx context.Context, scope ebooks.EnrichmentScope) (ebooks.EnrichmentRunResult, error)
}
type ebookMetadataTask struct {
enricher ebookMetadataEnricher
scope ebooks.EnrichmentScope
key string
name string
description string
triggers []taskmanager.TriggerConfig
budget time.Duration
now func() time.Time
errorPrefix string
}
// SyncEbookMetadataTask drains new and recurring ebook metadata work.
type SyncEbookMetadataTask struct {
*ebookMetadataTask
}
// BackfillEbookMetadataTask drains the legacy ebook backlog only when manually run.
type BackfillEbookMetadataTask struct {
*ebookMetadataTask
}
func NewSyncEbookMetadataTask(enricher ebookMetadataEnricher) *SyncEbookMetadataTask {
return &SyncEbookMetadataTask{ebookMetadataTask: &ebookMetadataTask{
enricher: enricher,
scope: ebooks.EnrichmentScopeIncremental,
key: "sync_ebook_metadata",
name: "Sync Ebook Metadata",
description: "Fetches metadata for new ebooks and ebooks due for a recurring refresh",
triggers: []taskmanager.TriggerConfig{{Type: taskmanager.TriggerTypeInterval, IntervalMs: 5 * 60 * 1000}},
budget: ebookMetadataExecutionBudget,
now: time.Now,
errorPrefix: "ebook metadata sync",
}}
}
func NewBackfillEbookMetadataTask(enricher ebookMetadataEnricher) *BackfillEbookMetadataTask {
return &BackfillEbookMetadataTask{ebookMetadataTask: &ebookMetadataTask{
enricher: enricher,
scope: ebooks.EnrichmentScopeLegacy,
key: "backfill_ebook_metadata",
name: "Backfill Ebook Metadata",
description: "Manually enriches the legacy ebook backlog without competing with scheduled metadata sync",
budget: ebookMetadataExecutionBudget,
now: time.Now,
errorPrefix: "ebook metadata backfill",
}}
}
func (t *ebookMetadataTask) Key() string { return t.key }
func (t *ebookMetadataTask) Name() string { return t.name }
func (t *ebookMetadataTask) Description() string { return t.description }
func (t *ebookMetadataTask) Category() taskmanager.TaskCategory {
return taskmanager.TaskCategoryMetadata
}
func (t *ebookMetadataTask) IsHidden() bool { return false }
func (t *ebookMetadataTask) DefaultTriggers() []taskmanager.TriggerConfig {
return append([]taskmanager.TriggerConfig(nil), t.triggers...)
}
func (t *ebookMetadataTask) Execute(ctx context.Context, progress taskmanager.ProgressReporter) error {
progress.Report(0, fmt.Sprintf("%s started", t.name))
started := t.now()
total := ebooks.EnrichmentRunResult{}
for {
if err := ctx.Err(); err != nil {
return err
}
batch, err := t.enricher.Run(ctx, t.scope)
if err != nil {
return fmt.Errorf("%s: %w", t.errorPrefix, err)
}
addEbookEnrichmentResult(&total, batch)
reportEbookEnrichmentProgress(progress, total, false)
if err := ctx.Err(); err != nil {
return err
}
if ebookEnrichmentBatchMadeNoProgress(batch) {
reportEbookEnrichmentCircuitBreak(progress, total)
return nil
}
if batch.Remaining == 0 {
reportEbookEnrichmentProgress(progress, total, true)
return nil
}
if batch.Claimed == 0 {
return nil
}
if t.now().Sub(started) >= t.budget {
total.Deferred += total.Remaining
reportEbookEnrichmentProgress(progress, total, false)
return nil
}
}
}
func ebookEnrichmentBatchMadeNoProgress(batch ebooks.EnrichmentRunResult) bool {
return batch.Claimed > 0 &&
batch.Enriched == 0 &&
batch.NoMatch == 0 &&
batch.Failed+batch.Deferred >= batch.Claimed
}
func addEbookEnrichmentResult(total *ebooks.EnrichmentRunResult, batch ebooks.EnrichmentRunResult) {
total.Claimed += batch.Claimed
total.Enriched += batch.Enriched
total.NoMatch += batch.NoMatch
total.Failed += batch.Failed
total.Deferred += batch.Deferred
total.Remaining = batch.Remaining
}
func reportEbookEnrichmentProgress(
progress taskmanager.ProgressReporter,
result ebooks.EnrichmentRunResult,
complete bool,
) {
data, _ := json.Marshal(result)
progress.SetResultData(data)
percent := ebookEnrichmentPercent(result)
if complete && result.Remaining == 0 {
percent = 100
}
progress.Report(percent, fmt.Sprintf(
"Claimed %d, enriched %d, no match %d, failed %d, deferred %d, remaining %d",
result.Claimed,
result.Enriched,
result.NoMatch,
result.Failed,
result.Deferred,
result.Remaining,
))
}
func reportEbookEnrichmentCircuitBreak(
progress taskmanager.ProgressReporter,
result ebooks.EnrichmentRunResult,
) {
data, _ := json.Marshal(result)
progress.SetResultData(data)
percent := ebookEnrichmentPercent(result)
if percent >= 100 {
percent = 99
}
progress.Report(percent, fmt.Sprintf(
"Paused after a full batch made no progress; remaining work will retry later. Claimed %d, failed %d, deferred %d, remaining %d",
result.Claimed,
result.Failed,
result.Deferred,
result.Remaining,
))
}
func ebookEnrichmentPercent(result ebooks.EnrichmentRunResult) float64 {
if result.Remaining == 0 {
return 100
}
total := result.Claimed + result.Remaining
if total <= 0 {
return 0
}
percent := float64(result.Claimed) * 100 / float64(total)
if percent >= 100 {
return 99
}
return percent
}