Owner review of p6 (Ornith tasks 01-09), with an independent reviewer pass whose findings I
confirmed against the code. Every fix has a regression test that fails on the old code.
internal/batch
- Close let the workers keep draining every live job under the cancelled context, failing
every target; they now stop at once, store nothing on a failure during shutdown, and leave
the job live for MarkInFlightFailed("restart").
- A job cancelled while its in-flight target came back busy stayed in the rotation forever
(depth gauge stuck, job held in memory); it now leaves. The task 03 design had this gap: mine.
- Submit's idempotency lookup, caps and create ran unlocked: 20 concurrent submits by one
caller made 8 jobs. Serialised with submitMu.
- A failed target insert left a queued job no worker would run (holding the caller's cap);
it is marked failed.
- A second concurrent cancel returned true and counted twice.
- Config: zero OR negative values take the defaults (negative Workers panicked the
WaitGroup; a negative TTL or byte budget would sweep or evict everything).
- Retry timing used the pre-fetch time (a 5 s busy answer made RetryWait moot).
- The rotation cursor is kept within [0, len(live)] (it was absolute and drifted on removal).
- The unused in-memory callers map is gone (Submit racing Close wrote to a nil map).
internal/dcache
- PutBatchTargets, SweepBatch and eviction are transactional; the sweep never removes a
queued or running job.
internal/api
- Every /v1/batch endpoint answers 503 when batch is off; POST panicked on the nil runner.
- GET /{id}/{idx} bounds idx by the rows actually returned.
- batchFetch copies a *fetch.Error instead of mutating one the PDF pool may share.
internal/config
- ParseSize no longer overflows to a negative size.
cmd/sift
- sift batch treated 202 Accepted (submit) as an error and exited 1 with no message.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
154 lines
3.8 KiB
Go
154 lines
3.8 KiB
Go
package config
|
|
|
|
import (
|
|
"math"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pelletier/go-toml/v2"
|
|
)
|
|
|
|
// File is the on-disk /etc/sift/config.toml.
|
|
type File struct {
|
|
Listen string `toml:"listen"`
|
|
UserAgent string `toml:"user_agent"`
|
|
DenyPrefixes []string `toml:"deny_prefixes"`
|
|
SelfIP SelfIP `toml:"self_ip"`
|
|
Dixie Dixie `toml:"dixie"`
|
|
Digest Digest `toml:"digest"`
|
|
Context Context `toml:"context"`
|
|
Search Search `toml:"search"`
|
|
Batch Batch `toml:"batch"`
|
|
}
|
|
|
|
// Batch is the [batch] block. Batch is on by default: no block means the runner runs with its own
|
|
// defaults. disabled = true turns it off and the endpoints answer 503.
|
|
type Batch struct {
|
|
Disabled bool `toml:"disabled"`
|
|
TargetsPerJob int `toml:"targets_per_job"`
|
|
JobsPerCaller int `toml:"jobs_per_caller"`
|
|
JobsTotal int `toml:"jobs_total"`
|
|
Workers int `toml:"workers"`
|
|
TTL string `toml:"ttl"`
|
|
MaxBytes string `toml:"max_bytes"`
|
|
}
|
|
|
|
type Dixie struct {
|
|
Chat string `toml:"chat"`
|
|
Embed string `toml:"embed"`
|
|
EmbedModel string `toml:"embed_model"`
|
|
MaxInflight int `toml:"max_inflight"`
|
|
QueueDepth int `toml:"queue_depth"`
|
|
QueueWait string `toml:"queue_wait"`
|
|
}
|
|
|
|
type Digest struct {
|
|
Model string `toml:"model"`
|
|
Thinking bool `toml:"thinking"`
|
|
Cache string `toml:"cache"`
|
|
}
|
|
|
|
type Context struct {
|
|
Honcho string `toml:"honcho"`
|
|
Workspaces []string `toml:"workspaces"`
|
|
PermitRestricted []string `toml:"permit_restricted"`
|
|
}
|
|
|
|
// Search is the [search] block. Sources default off; bake-off picks defaults.
|
|
type Search struct {
|
|
K int `toml:"k"`
|
|
Timeout string `toml:"timeout"`
|
|
Rerank *bool `toml:"rerank"`
|
|
Digest bool `toml:"digest"`
|
|
CacheTTL string `toml:"cache_ttl"`
|
|
SearxNG SearchSource `toml:"searxng"`
|
|
BraveLLM SearchSource `toml:"brave_llm"`
|
|
Tavily TavilySource `toml:"tavily"`
|
|
}
|
|
|
|
type SearchSource struct {
|
|
Enabled bool `toml:"enabled"`
|
|
URL string `toml:"url"`
|
|
NoRerank bool `toml:"no_rerank"`
|
|
}
|
|
|
|
type TavilySource struct {
|
|
Enabled bool `toml:"enabled"`
|
|
URL string `toml:"url"`
|
|
DailyCreditCap int `toml:"daily_credit_cap"`
|
|
NoRerank bool `toml:"no_rerank"`
|
|
}
|
|
|
|
func (s Search) RerankOn() bool {
|
|
if s.Rerank == nil {
|
|
return true
|
|
}
|
|
return *s.Rerank
|
|
}
|
|
|
|
func (s Search) AnyEnabled() bool {
|
|
return s.SearxNG.Enabled || s.BraveLLM.Enabled || s.Tavily.Enabled
|
|
}
|
|
|
|
// SelfIP is the [self_ip] block.
|
|
type SelfIP struct {
|
|
Sources []string `toml:"sources"`
|
|
Interval string `toml:"interval"`
|
|
KeepPrevious string `toml:"keep_previous"`
|
|
Path string `toml:"path"`
|
|
}
|
|
|
|
// Load reads path. A missing file returns a zero File and a nil error.
|
|
func Load(path string) (File, error) {
|
|
var f File
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return f, nil
|
|
}
|
|
return f, err
|
|
}
|
|
if err := toml.Unmarshal(b, &f); err != nil {
|
|
return File{}, err
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
func ParseDuration(s string, def time.Duration) time.Duration {
|
|
if s == "" {
|
|
return def
|
|
}
|
|
d, err := time.ParseDuration(s)
|
|
if err != nil {
|
|
return def
|
|
}
|
|
return d
|
|
}
|
|
|
|
// ParseSize parses a positive byte count: a plain integer, or one with a binary suffix KiB, MiB or
|
|
// GiB. Anything else, zero or negative returns def.
|
|
func ParseSize(s string, def int64) int64 {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return def
|
|
}
|
|
var mult int64
|
|
switch {
|
|
case strings.HasSuffix(s, "KiB"):
|
|
mult, s = 1<<10, s[:len(s)-3]
|
|
case strings.HasSuffix(s, "MiB"):
|
|
mult, s = 1<<20, s[:len(s)-3]
|
|
case strings.HasSuffix(s, "GiB"):
|
|
mult, s = 1<<30, s[:len(s)-3]
|
|
default:
|
|
mult = 1
|
|
}
|
|
n, err := strconv.ParseInt(s, 10, 64)
|
|
if err != nil || n <= 0 || n > math.MaxInt64/mult { // the last: n*mult would overflow
|
|
return def
|
|
}
|
|
return n * mult
|
|
}
|