Initial commit
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s
Assisted-by: GLM 5.3
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
|
||||
|
||||
// Package scheduler publishes scheduled posts whose publish_at date
|
||||
// has arrived, either from the in-app loop or the publish-due CLI.
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"sourcedock.dev/petrbalvin/interpres/v2"
|
||||
"sourcedock.dev/petrbalvin/volumen/internal/post"
|
||||
)
|
||||
|
||||
// Store is the content the scheduler reads and writes: every post, and a
|
||||
// save for the ones whose date has arrived.
|
||||
type Store interface {
|
||||
All() []*post.Post
|
||||
Save(p *post.Post) (*post.Post, error)
|
||||
}
|
||||
|
||||
// MinInterval is the shortest interval the in-app loop accepts.
|
||||
// time.NewTicker panics on a non-positive duration, so a bad
|
||||
// configuration is refused here instead of reaching the runtime.
|
||||
const MinInterval = time.Second
|
||||
|
||||
// PublishDueWith publishes every post whose publish_at date is today or
|
||||
// earlier and returns the slugs that changed, in store order, together with
|
||||
// the number of posts whose save failed. Publishing removes the publish_at
|
||||
// key and defaults date to it. The sink is called once per published post,
|
||||
// so the caller can deliver the same post.published webhook the admin
|
||||
// delivers; it may be nil. A post whose save fails is skipped, so one bad
|
||||
// file cannot stop the sweep.
|
||||
func PublishDueWith(st Store, onPublished func(*post.Post)) ([]string, int) {
|
||||
today := post.TodayUTC()
|
||||
var published []string
|
||||
failures := 0
|
||||
for _, cached := range st.All() {
|
||||
// Cached posts are shared with request handlers; mutate a clone.
|
||||
p := cached.Clone()
|
||||
due, ok := p.DueAt()
|
||||
if !ok || due.After(today) {
|
||||
continue
|
||||
}
|
||||
p.Metadata.Delete("publish_at")
|
||||
if _, hasDate := p.Metadata.Get("date"); !hasDate {
|
||||
p.Metadata.Set("date", interpres.LocalDate{Time: due})
|
||||
}
|
||||
if _, err := st.Save(p); err != nil {
|
||||
failures++
|
||||
slog.Warn("scheduler: cannot publish", "slug", p.Slug(), "error", err)
|
||||
continue
|
||||
}
|
||||
published = append(published, p.Slug())
|
||||
slog.Info("scheduler published", "slug", p.Slug())
|
||||
if onPublished != nil {
|
||||
onPublished(p)
|
||||
}
|
||||
}
|
||||
return published, failures
|
||||
}
|
||||
|
||||
// DuePosts lists the posts PublishDue would publish.
|
||||
func DuePosts(st Store) []*post.Post {
|
||||
today := post.TodayUTC()
|
||||
var due []*post.Post
|
||||
for _, p := range st.All() {
|
||||
if at, ok := p.DueAt(); ok && !at.After(today) {
|
||||
due = append(due, p)
|
||||
}
|
||||
}
|
||||
return due
|
||||
}
|
||||
|
||||
// Run publishes due posts once at start-up and then every interval
|
||||
// until stop is closed. An interval below MinInterval is reported and
|
||||
// the loop does not start.
|
||||
func Run(st Store, interval time.Duration, stop <-chan struct{}, onPublished func(*post.Post)) {
|
||||
if interval < MinInterval {
|
||||
slog.Error("scheduler: interval below the minimum, loop not started",
|
||||
"interval", interval, "minimum", MinInterval)
|
||||
return
|
||||
}
|
||||
slog.Info("scheduler started", "interval", interval)
|
||||
if published, failures := PublishDueWith(st, onPublished); len(published) > 0 || failures > 0 {
|
||||
slog.Info("scheduler: start-up sweep", "published", len(published), "failed", failures)
|
||||
}
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-ticker.C:
|
||||
PublishDueWith(st, onPublished)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user