Files
sift/internal/config/config.go
T
kyleandClaude Opus 5.5 a452aa16a1 P6 review: runner shutdown, cancel-while-busy, concurrent submits, batch-off 503, transactional store, CLI 202
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>
2026-09-26 23:27:47 -07:00

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
}