feat: schedule persistent archive jobs with console management
This commit is contained in:
@@ -3,8 +3,10 @@ package config
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
core "git.yuntuo.hk/k2safe/orbit/model"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -12,6 +14,10 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ArchiveRoot string // ArchiveRoot 私有持久归档卷绝对路径,空表示关闭后台执行器。
|
||||
ArchiveStorageID core.ID // ArchiveStorageID 32位小写十六进制卷身份,各节点须挂载同一物理卷。
|
||||
ArchiveMaxBytes int64 // ArchiveMaxBytes 全卷文件预算,默认10GiB。
|
||||
ArchiveTickInterval time.Duration // ArchiveTickInterval 后台扫描间隔,默认10秒。
|
||||
RetentionDefaults retention.Policy // RetentionDefaults 未保存数据库策略时使用的部署默认值。
|
||||
HTTPAddr string
|
||||
MySQLDSN string
|
||||
@@ -32,6 +38,7 @@ type Config struct {
|
||||
func Load() (Config, error) { return FromEnv(os.Getenv) }
|
||||
func FromEnv(get func(string) string) (Config, error) {
|
||||
c := Config{
|
||||
ArchiveRoot: get("ORBIT_ARCHIVE_ROOT"), ArchiveStorageID: core.ID(get("ORBIT_ARCHIVE_STORAGE_ID")), ArchiveMaxBytes: 10 << 30, ArchiveTickInterval: 10 * time.Second,
|
||||
HTTPAddr: get("ORBIT_HTTP_ADDR"),
|
||||
MySQLDSN: get("ORBIT_MYSQL_DSN"),
|
||||
MaxOpen: 20,
|
||||
@@ -123,6 +130,26 @@ func FromEnv(get func(string) string) (Config, error) {
|
||||
return Config{}, errors.New("invalid ORBIT_METRICS_ADDR")
|
||||
}
|
||||
}
|
||||
|
||||
if c.ArchiveRoot != "" || c.ArchiveStorageID != "" {
|
||||
if !filepath.IsAbs(c.ArchiveRoot) || c.ArchiveStorageID.Validate() != nil {
|
||||
return Config{}, errors.New("ORBIT_ARCHIVE_ROOT and ORBIT_ARCHIVE_STORAGE_ID must be configured together")
|
||||
}
|
||||
}
|
||||
if raw := get("ORBIT_ARCHIVE_MAX_BYTES"); raw != "" {
|
||||
n, err := strconv.ParseInt(raw, 10, 64)
|
||||
if err != nil || n < 1<<20 || n > 1<<40 {
|
||||
return Config{}, errors.New("ORBIT_ARCHIVE_MAX_BYTES must be 1MiB to 1TiB")
|
||||
}
|
||||
c.ArchiveMaxBytes = n
|
||||
}
|
||||
if raw := get("ORBIT_ARCHIVE_TICK_INTERVAL"); raw != "" {
|
||||
d, err := time.ParseDuration(raw)
|
||||
if err != nil || d < time.Second || d > time.Minute {
|
||||
return Config{}, errors.New("ORBIT_ARCHIVE_TICK_INTERVAL must be 1s to 1m")
|
||||
}
|
||||
c.ArchiveTickInterval = d
|
||||
}
|
||||
var err error
|
||||
c.RetentionDefaults, err = retention.FromEnv(get)
|
||||
if err != nil {
|
||||
|
||||
@@ -65,3 +65,28 @@ func TestMetricsAddressIsExplicitAndValidated(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveRuntimeRequiresBoundedPersistentVolumeConfig(t *testing.T) {
|
||||
base := map[string]string{"ORBIT_MYSQL_DSN": "test"}
|
||||
get := func(k string) string { return base[k] }
|
||||
cfg, err := FromEnv(get)
|
||||
if err != nil || cfg.ArchiveRoot != "" || cfg.ArchiveMaxBytes != 10<<30 {
|
||||
t.Fatal(cfg, err)
|
||||
}
|
||||
base["ORBIT_ARCHIVE_ROOT"] = "/private/archive"
|
||||
if _, err = FromEnv(get); err == nil {
|
||||
t.Fatal("missing volume identity accepted")
|
||||
}
|
||||
base["ORBIT_ARCHIVE_STORAGE_ID"] = "11111111111111111111111111111111"
|
||||
if _, err = FromEnv(get); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for k, v := range map[string]string{"ORBIT_ARCHIVE_MAX_BYTES": "0", "ORBIT_ARCHIVE_TICK_INTERVAL": "100ms", "ORBIT_ARCHIVE_ROOT": "relative", "ORBIT_ARCHIVE_STORAGE_ID": "not-an-id"} {
|
||||
old := base[k]
|
||||
base[k] = v
|
||||
if _, err = FromEnv(get); err == nil {
|
||||
t.Fatal("bad archive config", k)
|
||||
}
|
||||
base[k] = old
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user