docs: publish Preview 19 dogfood evidence
Export the reviewed allowlisted snapshot from private source commit 05928cebd01b586cf9e9d4b8c8537a7605a6068c. This records the exact candidate, bounded capacity result, stateful migration scratch requirement, authenticated batch identity proof, and immediate live acceptance evidence. AI-Assisted: OpenAI Codex Signed-off-by: Cole Speelman <crspeelman@gmail.com>
This commit is contained in:
@@ -0,0 +1,920 @@
|
||||
//go:build observatory_capacity_fixture && linux
|
||||
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
// Command capacitytest runs the bounded Observatory capacity and recovery
|
||||
// campaign. It is excluded from ordinary builds and emits only aggregate,
|
||||
// synthetic evidence.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"gamertan.com/observatory/internal/model"
|
||||
"gamertan.com/observatory/internal/query"
|
||||
"gamertan.com/observatory/internal/spool"
|
||||
"gamertan.com/observatory/internal/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
reportVersion = 2
|
||||
visibilityObservationTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
type settings struct {
|
||||
sustainRate int
|
||||
sustainDuration time.Duration
|
||||
burstRate int
|
||||
burstDuration time.Duration
|
||||
minimumPrimary int64
|
||||
queryIterations int
|
||||
organizations int
|
||||
primaryWeight int
|
||||
batchInterval time.Duration
|
||||
requireCgroup bool
|
||||
expectedCPUs int
|
||||
expectedMemory int64
|
||||
}
|
||||
|
||||
type phaseReport struct {
|
||||
TargetRate int `json:"target_rate_per_second"`
|
||||
DurationSeconds float64 `json:"target_duration_seconds"`
|
||||
Observations int64 `json:"observations"`
|
||||
ElapsedSeconds float64 `json:"elapsed_seconds"`
|
||||
AchievedRate float64 `json:"achieved_rate_per_second"`
|
||||
IngestP50Milliseconds float64 `json:"ingest_p50_milliseconds"`
|
||||
IngestP95Milliseconds float64 `json:"ingest_p95_milliseconds"`
|
||||
IngestP99Milliseconds float64 `json:"ingest_p99_milliseconds"`
|
||||
VisibleP50Milliseconds float64 `json:"visible_p50_milliseconds"`
|
||||
VisibleP95Milliseconds float64 `json:"visible_p95_milliseconds"`
|
||||
VisibleP99Milliseconds float64 `json:"visible_p99_milliseconds"`
|
||||
}
|
||||
|
||||
type queryReport struct {
|
||||
Name string `json:"name"`
|
||||
Iterations int `json:"iterations"`
|
||||
P50Milliseconds float64 `json:"p50_milliseconds"`
|
||||
P95Milliseconds float64 `json:"p95_milliseconds"`
|
||||
P99Milliseconds float64 `json:"p99_milliseconds"`
|
||||
MaximumScannedRows int64 `json:"maximum_scanned_rows"`
|
||||
MaximumScannedBytes int64 `json:"maximum_scanned_bytes"`
|
||||
}
|
||||
|
||||
type spoolReport struct {
|
||||
Batches int `json:"batches"`
|
||||
Observations int64 `json:"observations"`
|
||||
OldestAgeHours float64 `json:"oldest_age_hours"`
|
||||
Replayed int64 `json:"replayed_observations"`
|
||||
RemainingAfterAck int `json:"remaining_after_ack"`
|
||||
DuplicateRecognized bool `json:"duplicate_recognized"`
|
||||
}
|
||||
|
||||
type retentionEvidence struct {
|
||||
ArchivedSegments int `json:"archived_segments"`
|
||||
ArchivedBytes int64 `json:"archived_bytes"`
|
||||
RemovedSegments int `json:"removed_segments"`
|
||||
RemovedBytes int64 `json:"removed_bytes"`
|
||||
ProjectionRowsRemoved int64 `json:"projection_rows_removed"`
|
||||
ColdQueryRows int `json:"cold_query_rows"`
|
||||
}
|
||||
|
||||
type resourceReport struct {
|
||||
GOMAXPROCS int `json:"gomaxprocs"`
|
||||
CPUQuota float64 `json:"cpu_quota"`
|
||||
MemoryLimitBytes int64 `json:"memory_limit_bytes"`
|
||||
MaximumRSSBytes int64 `json:"maximum_rss_bytes"`
|
||||
DatasetBytes int64 `json:"dataset_bytes"`
|
||||
}
|
||||
|
||||
type projectionDrainReport struct {
|
||||
PendingSegments int `json:"pending_segments_at_start"`
|
||||
PendingBytes int64 `json:"pending_bytes_at_start"`
|
||||
OldestLagSeconds float64 `json:"oldest_pending_lag_seconds_at_start"`
|
||||
ElapsedSeconds float64 `json:"elapsed_seconds"`
|
||||
}
|
||||
|
||||
type storageClass struct {
|
||||
Files int `json:"files"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
}
|
||||
|
||||
type storageBreakdown struct {
|
||||
Raw storageClass `json:"raw_segments"`
|
||||
Projection storageClass `json:"projection_sqlite"`
|
||||
Control storageClass `json:"control_sqlite"`
|
||||
Other storageClass `json:"other"`
|
||||
Total storageClass `json:"total"`
|
||||
SQLitePageClasses sqlitePageBreakdown `json:"primary_projection_sqlite_page_classes"`
|
||||
SQLiteBytes map[string]int64 `json:"primary_projection_sqlite_objects"`
|
||||
}
|
||||
|
||||
type sqlitePageBreakdown struct {
|
||||
Tables int64 `json:"tables_bytes"`
|
||||
Indexes int64 `json:"indexes_bytes"`
|
||||
Internal int64 `json:"internal_bytes"`
|
||||
Total int64 `json:"total_bytes"`
|
||||
}
|
||||
|
||||
type campaignReport struct {
|
||||
Version int `json:"version"`
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
CompletedAt time.Time `json:"completed_at"`
|
||||
Organizations int `json:"organizations"`
|
||||
PrimaryPhaseWeight int `json:"primary_phase_weight"`
|
||||
TotalObservations int64 `json:"total_observations"`
|
||||
PrimaryObservations int64 `json:"primary_observations"`
|
||||
Sustain phaseReport `json:"sustain"`
|
||||
Burst phaseReport `json:"burst"`
|
||||
FillSeconds float64 `json:"fill_seconds"`
|
||||
ProjectionDrain projectionDrainReport `json:"projection_drain"`
|
||||
PrimaryStorage storageBreakdown `json:"primary_storage"`
|
||||
Queries []queryReport `json:"queries"`
|
||||
Spool spoolReport `json:"spool"`
|
||||
Retention retentionEvidence `json:"retention"`
|
||||
Resources resourceReport `json:"resources"`
|
||||
Pass bool `json:"pass"`
|
||||
}
|
||||
|
||||
type sourceState struct {
|
||||
index int
|
||||
weight int64
|
||||
organizationID string
|
||||
sourceID string
|
||||
token string
|
||||
sequences map[model.Signal]uint64
|
||||
count atomic.Int64
|
||||
}
|
||||
|
||||
type measurements struct {
|
||||
mu sync.Mutex
|
||||
ingest []time.Duration
|
||||
visibility []time.Duration
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "capacity campaign failed:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run() error {
|
||||
configuration := settings{}
|
||||
flag.IntVar(&configuration.sustainRate, "sustain-rate", 2_000, "sustained mixed observations per second")
|
||||
flag.DurationVar(&configuration.sustainDuration, "sustain-duration", time.Hour, "sustained campaign duration")
|
||||
flag.IntVar(&configuration.burstRate, "burst-rate", 10_000, "burst observations per second")
|
||||
flag.DurationVar(&configuration.burstDuration, "burst-duration", time.Minute, "burst campaign duration")
|
||||
flag.Int64Var(&configuration.minimumPrimary, "minimum-primary-observations", 10_000_000, "minimum observations in the primary organization")
|
||||
flag.IntVar(&configuration.queryIterations, "query-iterations", 20, "iterations per common query")
|
||||
flag.IntVar(&configuration.organizations, "organizations", 4, "concurrent organizations")
|
||||
flag.IntVar(&configuration.primaryWeight, "primary-phase-weight", 1, "relative phase weight of the primary organization")
|
||||
flag.DurationVar(&configuration.batchInterval, "batch-interval", 500*time.Millisecond, "batch scheduling interval")
|
||||
flag.BoolVar(&configuration.requireCgroup, "require-cgroup", false, "require exact CPU and memory cgroup limits")
|
||||
flag.IntVar(&configuration.expectedCPUs, "expected-cpus", 4, "required CPU quota")
|
||||
flag.Int64Var(&configuration.expectedMemory, "expected-memory-bytes", 8<<30, "required memory limit")
|
||||
flag.Parse()
|
||||
if flag.NArg() != 0 {
|
||||
return errors.New("capacity campaign accepts no positional arguments")
|
||||
}
|
||||
if err := configuration.validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
cpuQuota, memoryLimit, err := cgroupLimits()
|
||||
if err != nil && configuration.requireCgroup {
|
||||
return err
|
||||
}
|
||||
if configuration.requireCgroup && (math.Abs(cpuQuota-float64(configuration.expectedCPUs)) > 0.001 || memoryLimit != configuration.expectedMemory || runtime.GOMAXPROCS(0) != configuration.expectedCPUs) {
|
||||
return fmt.Errorf("resource boundary differs: cpu=%.3f memory=%d gomaxprocs=%d", cpuQuota, memoryLimit, runtime.GOMAXPROCS(0))
|
||||
}
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
root, err := os.MkdirTemp("", "observatory-capacity-")
|
||||
if err != nil {
|
||||
return errors.New("create capacity workspace")
|
||||
}
|
||||
defer os.RemoveAll(root)
|
||||
if err = os.Chmod(root, 0o700); err != nil {
|
||||
return errors.New("protect capacity workspace")
|
||||
}
|
||||
store, err := storage.Open(filepath.Join(root, "dataset"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer store.Close()
|
||||
projectorContext, stopProjector := context.WithCancel(ctx)
|
||||
projectorDone := make(chan struct{})
|
||||
projectorErrors := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(projectorDone)
|
||||
store.RunProjector(projectorContext, 100*time.Millisecond, func(projectErr error) {
|
||||
select {
|
||||
case projectorErrors <- projectErr:
|
||||
default:
|
||||
}
|
||||
})
|
||||
}()
|
||||
defer func() {
|
||||
stopProjector()
|
||||
<-projectorDone
|
||||
}()
|
||||
states, err := createSources(ctx, store, configuration.organizations, configuration.primaryWeight)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report := campaignReport{Version: reportVersion, StartedAt: time.Now().UTC(), Organizations: len(states), PrimaryPhaseWeight: configuration.primaryWeight}
|
||||
seed := &atomic.Uint64{}
|
||||
fmt.Fprintln(os.Stderr, "stage=sustain")
|
||||
report.Sustain, err = runPhase(ctx, store, states, seed, configuration.sustainRate, configuration.sustainDuration, configuration.batchInterval)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "stage=burst")
|
||||
report.Burst, err = runPhase(ctx, store, states, seed, configuration.burstRate, configuration.burstDuration, configuration.batchInterval)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "stage=fill")
|
||||
fillStarted := time.Now()
|
||||
if err = fillPrimary(ctx, store, states[0], seed, configuration.minimumPrimary); err != nil {
|
||||
return err
|
||||
}
|
||||
report.FillSeconds = time.Since(fillStarted).Seconds()
|
||||
report.PrimaryObservations = states[0].count.Load()
|
||||
for _, state := range states {
|
||||
report.TotalObservations += state.count.Load()
|
||||
}
|
||||
if report.PrimaryObservations < configuration.minimumPrimary {
|
||||
return errors.New("primary dataset did not reach the required observation count")
|
||||
}
|
||||
drainStarted := time.Now()
|
||||
drainStart, statusErr := store.ProjectionStatus(ctx, drainStarted.UTC())
|
||||
if statusErr != nil {
|
||||
return statusErr
|
||||
}
|
||||
report.ProjectionDrain = projectionDrainReport{
|
||||
PendingSegments: drainStart.PendingSegments,
|
||||
PendingBytes: drainStart.PendingBytes,
|
||||
OldestLagSeconds: drainStart.OldestPendingLag.Seconds(),
|
||||
}
|
||||
if err = waitForProjection(ctx, store, projectorErrors, 10*time.Minute); err != nil {
|
||||
return err
|
||||
}
|
||||
report.ProjectionDrain.ElapsedSeconds = time.Since(drainStarted).Seconds()
|
||||
fmt.Fprintln(os.Stderr, "stage=queries")
|
||||
report.Queries, err = runQueries(ctx, store, states[0].organizationID, configuration.queryIterations)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report.PrimaryStorage, err = measureStorage(filepath.Join(root, "dataset"), states[0].organizationID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "stage=spool")
|
||||
report.Spool, err = runSpoolReplay(ctx, filepath.Join(root, "outage"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(os.Stderr, "stage=retention")
|
||||
report.Retention, err = runRetention(ctx, filepath.Join(root, "retention"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
report.Resources = resourceReport{GOMAXPROCS: runtime.GOMAXPROCS(0), CPUQuota: cpuQuota, MemoryLimitBytes: memoryLimit}
|
||||
report.Resources.MaximumRSSBytes = maximumRSS()
|
||||
report.Resources.DatasetBytes, err = directoryBytes(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var campaignErrors []error
|
||||
if report.Sustain.AchievedRate < float64(configuration.sustainRate)*0.99 || report.Burst.AchievedRate < float64(configuration.burstRate)*0.99 {
|
||||
campaignErrors = append(campaignErrors, errors.New("target observation rate was not sustained"))
|
||||
}
|
||||
if report.Sustain.VisibleP95Milliseconds >= 2_000 || report.Burst.VisibleP95Milliseconds >= 2_000 {
|
||||
campaignErrors = append(campaignErrors, errors.New("p95 ingestion-to-query visibility exceeded two seconds"))
|
||||
}
|
||||
for _, result := range report.Queries {
|
||||
if result.P95Milliseconds >= 3_000 {
|
||||
campaignErrors = append(campaignErrors, fmt.Errorf("query %s exceeded the three-second p95 boundary", result.Name))
|
||||
}
|
||||
}
|
||||
if configuration.requireCgroup && report.Resources.MaximumRSSBytes >= configuration.expectedMemory {
|
||||
campaignErrors = append(campaignErrors, errors.New("maximum RSS reached the cgroup memory boundary"))
|
||||
}
|
||||
report.CompletedAt = time.Now().UTC()
|
||||
report.Pass = len(campaignErrors) == 0
|
||||
encoder := json.NewEncoder(os.Stdout)
|
||||
encoder.SetIndent("", " ")
|
||||
if err = encoder.Encode(report); err != nil {
|
||||
campaignErrors = append(campaignErrors, fmt.Errorf("encode capacity report: %w", err))
|
||||
}
|
||||
return errors.Join(campaignErrors...)
|
||||
}
|
||||
|
||||
func (configuration settings) validate() error {
|
||||
if configuration.sustainRate < 1 || configuration.burstRate < configuration.sustainRate || configuration.sustainDuration < time.Second || configuration.burstDuration < time.Second || configuration.minimumPrimary < 1 || configuration.queryIterations < 1 || configuration.queryIterations > 100 || configuration.organizations != 4 || configuration.primaryWeight < 1 || configuration.primaryWeight > 32 || configuration.batchInterval < 100*time.Millisecond || configuration.batchInterval > time.Second || configuration.expectedCPUs < 1 || configuration.expectedMemory < 1<<30 {
|
||||
return errors.New("capacity campaign settings are invalid")
|
||||
}
|
||||
primaryShare := float64(configuration.primaryWeight) / float64(configuration.primaryWeight+configuration.organizations-1)
|
||||
for _, rate := range []int{configuration.sustainRate, configuration.burstRate} {
|
||||
if int(math.Ceil(float64(rate)*configuration.batchInterval.Seconds()*primaryShare)) > model.MaxRecords {
|
||||
return errors.New("weighted batch would exceed the model record limit")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func createSources(ctx context.Context, store *storage.Store, count, primaryWeight int) ([]*sourceState, error) {
|
||||
states := make([]*sourceState, 0, count)
|
||||
for index := 0; index < count; index++ {
|
||||
state := &sourceState{index: index, weight: 1, organizationID: fmt.Sprintf("capacity-org-%d", index+1), sourceID: fmt.Sprintf("capacity-source-%d", index+1), sequences: map[model.Signal]uint64{}}
|
||||
if index == 0 {
|
||||
state.weight = int64(primaryWeight)
|
||||
}
|
||||
var err error
|
||||
state.token, err = store.CreateSource(ctx, state.sourceID, model.Scope{OrganizationID: state.organizationID, ProjectID: "observatory", EnvironmentID: "capacity", ServiceID: "server"})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
states = append(states, state)
|
||||
}
|
||||
return states, nil
|
||||
}
|
||||
|
||||
func runPhase(ctx context.Context, store *storage.Store, states []*sourceState, seed *atomic.Uint64, rate int, duration, interval time.Duration) (phaseReport, error) {
|
||||
target := int64(math.Round(float64(rate) * duration.Seconds()))
|
||||
batches := int(math.Ceil(float64(duration) / float64(interval)))
|
||||
started := time.Now()
|
||||
measure := &measurements{}
|
||||
errorsChannel := make(chan error, len(states))
|
||||
visibilityExpected := make(chan time.Time, batches)
|
||||
visibilityErrors := make(chan error, 1)
|
||||
visibilityDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(visibilityDone)
|
||||
for expected := range visibilityExpected {
|
||||
visible, err := latestVisibility(ctx, store, states[0].organizationID, expected)
|
||||
if err != nil {
|
||||
select {
|
||||
case visibilityErrors <- err:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
measure.mu.Lock()
|
||||
measure.visibility = append(measure.visibility, visible)
|
||||
measure.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
var wait sync.WaitGroup
|
||||
remaining := target
|
||||
weightTotal := int64(0)
|
||||
for _, state := range states {
|
||||
weightTotal += state.weight
|
||||
}
|
||||
for index, state := range states {
|
||||
stateTarget := target * state.weight / weightTotal
|
||||
if index == 0 {
|
||||
allocated := int64(0)
|
||||
for _, candidate := range states {
|
||||
allocated += target * candidate.weight / weightTotal
|
||||
}
|
||||
stateTarget += target - allocated
|
||||
}
|
||||
remaining -= stateTarget
|
||||
wait.Add(1)
|
||||
go func(current *sourceState, count int64) {
|
||||
defer wait.Done()
|
||||
if err := runScheduledSource(ctx, store, current, seed, count, batches, interval, started, measure, visibilityExpected); err != nil {
|
||||
errorsChannel <- err
|
||||
}
|
||||
}(state, stateTarget)
|
||||
}
|
||||
if remaining != 0 {
|
||||
return phaseReport{}, errors.New("phase allocation did not preserve target")
|
||||
}
|
||||
wait.Wait()
|
||||
elapsed := time.Since(started)
|
||||
close(visibilityExpected)
|
||||
<-visibilityDone
|
||||
close(errorsChannel)
|
||||
for phaseErr := range errorsChannel {
|
||||
if phaseErr != nil {
|
||||
return phaseReport{}, phaseErr
|
||||
}
|
||||
}
|
||||
select {
|
||||
case visibilityErr := <-visibilityErrors:
|
||||
return phaseReport{}, visibilityErr
|
||||
default:
|
||||
}
|
||||
measure.mu.Lock()
|
||||
ingest := append([]time.Duration(nil), measure.ingest...)
|
||||
visibility := append([]time.Duration(nil), measure.visibility...)
|
||||
measure.mu.Unlock()
|
||||
if len(ingest) == 0 || len(visibility) == 0 {
|
||||
return phaseReport{}, errors.New("phase produced no latency evidence")
|
||||
}
|
||||
return phaseReport{
|
||||
TargetRate: rate, DurationSeconds: duration.Seconds(), Observations: target, ElapsedSeconds: elapsed.Seconds(), AchievedRate: float64(target) / elapsed.Seconds(),
|
||||
IngestP50Milliseconds: milliseconds(percentile(ingest, 0.50)), IngestP95Milliseconds: milliseconds(percentile(ingest, 0.95)), IngestP99Milliseconds: milliseconds(percentile(ingest, 0.99)),
|
||||
VisibleP50Milliseconds: milliseconds(percentile(visibility, 0.50)), VisibleP95Milliseconds: milliseconds(percentile(visibility, 0.95)), VisibleP99Milliseconds: milliseconds(percentile(visibility, 0.99)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func runScheduledSource(ctx context.Context, store *storage.Store, state *sourceState, seed *atomic.Uint64, target int64, batches int, interval time.Duration, phaseStart time.Time, measurements *measurements, visibilityExpected chan<- time.Time) error {
|
||||
base, remainder := target/int64(batches), target%int64(batches)
|
||||
for batchIndex := 0; batchIndex < batches; batchIndex++ {
|
||||
planned := phaseStart.Add(time.Duration(batchIndex) * interval)
|
||||
if delay := time.Until(planned); delay > 0 {
|
||||
timer := time.NewTimer(delay)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
count := base
|
||||
if int64(batchIndex) < remainder {
|
||||
count++
|
||||
}
|
||||
if count == 0 {
|
||||
continue
|
||||
}
|
||||
signal := []model.Signal{model.SignalLogs, model.SignalMetrics, model.SignalTraces}[batchIndex%3]
|
||||
observed := time.Now().UTC()
|
||||
records := syntheticRecords(int(count), signal, observed, seed)
|
||||
state.sequences[signal]++
|
||||
batch := model.Batch{Version: model.BatchVersion, SourceID: state.sourceID, StreamID: string(signal), Sequence: state.sequences[signal], ObservedAt: observed, Signal: signal, Records: records}
|
||||
ingestStarted := time.Now()
|
||||
ack, err := store.Ingest(ctx, state.token, batch, observed)
|
||||
latency := time.Since(ingestStarted)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expected, err := batch.Digest()
|
||||
if err != nil || ack.BatchDigest != expected || ack.Duplicate {
|
||||
return errors.New("ingestion acknowledgement did not bind the scheduled batch")
|
||||
}
|
||||
state.count.Add(count)
|
||||
measurements.mu.Lock()
|
||||
measurements.ingest = append(measurements.ingest, latency)
|
||||
measurements.mu.Unlock()
|
||||
if state.index == 0 && signal == model.SignalLogs {
|
||||
visibilityExpected <- observed
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func syntheticRecords(count int, signal model.Signal, observed time.Time, seed *atomic.Uint64) []model.Observation {
|
||||
records := make([]model.Observation, count)
|
||||
routes := []string{"/", "/items", "/search", "/healthz"}
|
||||
for index := range records {
|
||||
id := seed.Add(1)
|
||||
timestamp := observed.Add(time.Duration(index) * time.Nanosecond)
|
||||
switch signal {
|
||||
case model.SignalLogs:
|
||||
status := "200"
|
||||
if id%50 == 0 {
|
||||
status = "503"
|
||||
}
|
||||
records[index] = model.Observation{Timestamp: timestamp, Name: "http.server.request", Severity: "information", CorrelationID: fmt.Sprintf("capacity-%d", id), Attributes: map[string]string{"http.route": routes[id%uint64(len(routes))], "http.status_code": status, "duration_ns": strconv.FormatUint(100_000+id%5_000_000, 10)}}
|
||||
case model.SignalMetrics:
|
||||
value := float64(id%10_000) / 100
|
||||
records[index] = model.Observation{Timestamp: timestamp, Name: "system.cpu.utilization", Value: &value}
|
||||
case model.SignalTraces:
|
||||
records[index] = model.Observation{Timestamp: timestamp, Name: "http.server", TraceID: fmt.Sprintf("%032x", id), SpanID: fmt.Sprintf("%016x", id), Attributes: map[string]string{"http.route": routes[id%uint64(len(routes))]}}
|
||||
}
|
||||
}
|
||||
return records
|
||||
}
|
||||
|
||||
func latestVisibility(ctx context.Context, store *storage.Store, organizationID string, expected time.Time) (time.Duration, error) {
|
||||
ast, err := query.Parse("logs | sort timestamp desc | limit 1", 10)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
// The release boundary is the aggregate p95 below two seconds, not a
|
||||
// zero-outlier maximum. Keep observing a slow sample long enough to retain
|
||||
// the phase evidence; the report gate below still fails an excessive p95.
|
||||
deadline := time.Now().Add(visibilityObservationTimeout)
|
||||
for {
|
||||
result, queryErr := store.Query(ctx, ast, query.Scope{OrganizationID: organizationID}, capacityBudget(10), time.Now().UTC())
|
||||
if queryErr == nil && len(result.Rows) == 1 {
|
||||
var timestamp string
|
||||
for index, column := range result.Columns {
|
||||
if column.Field == "timestamp" && result.Rows[0].Values[index] != nil {
|
||||
timestamp = *result.Rows[0].Values[index]
|
||||
}
|
||||
}
|
||||
visibleAt, parseErr := time.Parse(time.RFC3339Nano, timestamp)
|
||||
if parseErr == nil && !visibleAt.Before(expected) {
|
||||
return time.Since(expected), nil
|
||||
}
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
return 0, fmt.Errorf("latest visibility query remained stale for %s", visibilityObservationTimeout)
|
||||
}
|
||||
timer := time.NewTimer(10 * time.Millisecond)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return 0, ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitForProjection(ctx context.Context, store *storage.Store, projectorErrors <-chan error, maximum time.Duration) error {
|
||||
deadline := time.Now().Add(maximum)
|
||||
for {
|
||||
select {
|
||||
case err := <-projectorErrors:
|
||||
return fmt.Errorf("background projection failed: %w", err)
|
||||
default:
|
||||
}
|
||||
status, err := store.ProjectionStatus(ctx, time.Now().UTC())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status.PendingSegments == 0 {
|
||||
return nil
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
return fmt.Errorf("projection backlog remained after %s: segments=%d bytes=%d lag=%s", maximum, status.PendingSegments, status.PendingBytes, status.OldestPendingLag)
|
||||
}
|
||||
timer := time.NewTimer(100 * time.Millisecond)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fillPrimary(ctx context.Context, store *storage.Store, state *sourceState, seed *atomic.Uint64, minimum int64) error {
|
||||
batchIndex := 0
|
||||
for state.count.Load() < minimum {
|
||||
remaining := minimum - state.count.Load()
|
||||
count := int64(model.MaxRecords)
|
||||
if remaining < count {
|
||||
count = remaining
|
||||
}
|
||||
signal := []model.Signal{model.SignalLogs, model.SignalMetrics, model.SignalTraces}[batchIndex%3]
|
||||
observed := time.Now().UTC()
|
||||
state.sequences[signal]++
|
||||
batch := model.Batch{Version: model.BatchVersion, SourceID: state.sourceID, StreamID: string(signal), Sequence: state.sequences[signal], ObservedAt: observed, Signal: signal, Records: syntheticRecords(int(count), signal, observed, seed)}
|
||||
ack, err := store.Ingest(ctx, state.token, batch, observed)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expected, _ := batch.Digest()
|
||||
if ack.BatchDigest != expected || ack.Duplicate {
|
||||
return errors.New("fill acknowledgement did not bind the batch")
|
||||
}
|
||||
state.count.Add(count)
|
||||
batchIndex++
|
||||
if batchIndex%100 == 0 {
|
||||
fmt.Fprintf(os.Stderr, "stage=fill observations=%d\n", state.count.Load())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runQueries(ctx context.Context, store *storage.Store, organizationID string, iterations int) ([]queryReport, error) {
|
||||
definitions := []struct{ name, text string }{
|
||||
{"recent-errors-by-route", `logs | where status >= 500 | window 24h | summarize count() by route, window(5m) | sort count desc | limit 50`},
|
||||
{"recent-items", `logs | where route == "/items" | window 24h | sort timestamp desc | limit 50`},
|
||||
{"metric-rollup", `metrics | window 24h | summarize count(), p95(value) by name, window(5m) | sort count desc | limit 50`},
|
||||
}
|
||||
reports := make([]queryReport, 0, len(definitions))
|
||||
for _, definition := range definitions {
|
||||
ast, err := query.Parse(definition.text, 50)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var samples []time.Duration
|
||||
var maximumRows, maximumBytes int64
|
||||
for iteration := 0; iteration < iterations; iteration++ {
|
||||
started := time.Now()
|
||||
result, queryErr := store.Query(ctx, ast, query.Scope{OrganizationID: organizationID}, capacityBudget(50), time.Now().UTC())
|
||||
samples = append(samples, time.Since(started))
|
||||
if queryErr != nil {
|
||||
return nil, fmt.Errorf("capacity query %s failed: %w", definition.name, queryErr)
|
||||
}
|
||||
if len(result.Rows) == 0 {
|
||||
return nil, fmt.Errorf("capacity query %s returned no rows", definition.name)
|
||||
}
|
||||
maximumRows = max(maximumRows, int64(result.Stats.ScannedRows))
|
||||
maximumBytes = max(maximumBytes, result.Stats.ScannedBytes)
|
||||
}
|
||||
reports = append(reports, queryReport{Name: definition.name, Iterations: iterations, P50Milliseconds: milliseconds(percentile(samples, 0.50)), P95Milliseconds: milliseconds(percentile(samples, 0.95)), P99Milliseconds: milliseconds(percentile(samples, 0.99)), MaximumScannedRows: maximumRows, MaximumScannedBytes: maximumBytes})
|
||||
}
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
func runSpoolReplay(ctx context.Context, root string) (spoolReport, error) {
|
||||
now := time.Now().UTC()
|
||||
queue, err := spool.Open(filepath.Join(root, "spool"), 1<<30, 72*time.Hour)
|
||||
if err != nil {
|
||||
return spoolReport{}, err
|
||||
}
|
||||
store, err := storage.Open(filepath.Join(root, "server"))
|
||||
if err != nil {
|
||||
return spoolReport{}, err
|
||||
}
|
||||
defer store.Close()
|
||||
token, err := store.CreateSource(ctx, "outage-source", model.Scope{OrganizationID: "outage-org", ProjectID: "observatory", EnvironmentID: "capacity", ServiceID: "agent"})
|
||||
if err != nil {
|
||||
return spoolReport{}, err
|
||||
}
|
||||
const batches, recordsPerBatch = 72, 100
|
||||
for index := 0; index < batches; index++ {
|
||||
observed := now.Add(-time.Duration(batches-index) * time.Hour).Add(time.Minute)
|
||||
batch := model.Batch{Version: model.BatchVersion, SourceID: "outage-source", StreamID: "logs", Sequence: uint64(index + 1), ObservedAt: observed, Signal: model.SignalLogs, Records: syntheticRecords(recordsPerBatch, model.SignalLogs, observed, &atomic.Uint64{})}
|
||||
entry, putErr := queue.Put(batch, observed)
|
||||
if putErr != nil {
|
||||
return spoolReport{}, putErr
|
||||
}
|
||||
if err = os.Chtimes(entry.Path, observed, observed); err != nil {
|
||||
return spoolReport{}, err
|
||||
}
|
||||
}
|
||||
entries, err := queue.List(now)
|
||||
if err != nil || len(entries) != batches {
|
||||
return spoolReport{}, errors.New("72-hour spool did not preserve every batch")
|
||||
}
|
||||
report := spoolReport{Batches: len(entries), Observations: batches * recordsPerBatch, OldestAgeHours: now.Sub(entries[0].ModTime).Hours()}
|
||||
for index, entry := range entries {
|
||||
batch, readErr := queue.Read(entry)
|
||||
if readErr != nil {
|
||||
return spoolReport{}, readErr
|
||||
}
|
||||
ack, ingestErr := store.Ingest(ctx, token, batch, now)
|
||||
if ingestErr != nil {
|
||||
return spoolReport{}, ingestErr
|
||||
}
|
||||
expected, _ := batch.Digest()
|
||||
if ack.BatchDigest != expected {
|
||||
return spoolReport{}, errors.New("outage replay acknowledgement mismatch")
|
||||
}
|
||||
if index == 0 {
|
||||
duplicate, duplicateErr := store.Ingest(ctx, token, batch, now)
|
||||
if duplicateErr != nil || !duplicate.Duplicate || duplicate.BatchDigest != expected {
|
||||
return spoolReport{}, errors.New("outage replay duplicate was not recognized")
|
||||
}
|
||||
report.DuplicateRecognized = true
|
||||
}
|
||||
if err = queue.Acknowledge(entry, entry.Digest); err != nil {
|
||||
return spoolReport{}, err
|
||||
}
|
||||
report.Replayed += int64(len(batch.Records))
|
||||
}
|
||||
remaining, err := queue.List(now)
|
||||
if err != nil {
|
||||
return spoolReport{}, err
|
||||
}
|
||||
report.RemainingAfterAck = len(remaining)
|
||||
if report.Replayed != report.Observations || report.RemainingAfterAck != 0 || report.OldestAgeHours < 71.9 {
|
||||
return spoolReport{}, errors.New("outage replay evidence is incomplete")
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func runRetention(ctx context.Context, root string) (retentionEvidence, error) {
|
||||
now := time.Now().UTC()
|
||||
store, err := storage.Open(root)
|
||||
if err != nil {
|
||||
return retentionEvidence{}, err
|
||||
}
|
||||
defer store.Close()
|
||||
token, err := store.CreateSource(ctx, "retention-source", model.Scope{OrganizationID: "retention-org", ProjectID: "observatory", EnvironmentID: "capacity", ServiceID: "server"})
|
||||
if err != nil {
|
||||
return retentionEvidence{}, err
|
||||
}
|
||||
tests := []struct {
|
||||
signal model.Signal
|
||||
age time.Duration
|
||||
}{
|
||||
{model.SignalLogs, 31 * 24 * time.Hour},
|
||||
{model.SignalTraces, 31 * 24 * time.Hour},
|
||||
{model.SignalMetrics, 15 * 24 * time.Hour},
|
||||
{model.SignalDeployments, 399 * 24 * time.Hour},
|
||||
}
|
||||
for index, item := range tests {
|
||||
observed := now.Add(-item.age)
|
||||
batch := model.Batch{Version: model.BatchVersion, SourceID: "retention-source", StreamID: string(item.signal), Sequence: 1, ObservedAt: now, Signal: item.signal, Records: syntheticRecords(1, item.signal, observed, &atomic.Uint64{})}
|
||||
if item.signal == model.SignalDeployments {
|
||||
batch.Records[0] = model.Observation{Timestamp: observed, Name: "deployment", Attributes: map[string]string{"outcome": "success"}}
|
||||
}
|
||||
if _, err = store.Ingest(ctx, token, batch, now); err != nil {
|
||||
return retentionEvidence{}, fmt.Errorf("retention fixture %d: %w", index, err)
|
||||
}
|
||||
}
|
||||
if err = store.Recover(ctx); err != nil {
|
||||
return retentionEvidence{}, err
|
||||
}
|
||||
policy := storage.RetentionPolicy{RawLogsDays: 30, RawTracesDays: 30, RawMetricsDays: 14, ColdRawDays: 400, DeleteColdRaw: true, MetricRollupsDays: 400, EvidenceDays: 400}
|
||||
report, err := store.ApplyRetention(ctx, policy, now.Add(2*24*time.Hour))
|
||||
if err != nil {
|
||||
return retentionEvidence{}, err
|
||||
}
|
||||
ast, err := query.Parse("logs | window 960h | limit 10", 10)
|
||||
if err != nil {
|
||||
return retentionEvidence{}, err
|
||||
}
|
||||
result, err := store.Query(ctx, ast, query.Scope{OrganizationID: "retention-org", Sensitive: true}, capacityBudget(10), now.Add(2*24*time.Hour))
|
||||
if err != nil {
|
||||
return retentionEvidence{}, err
|
||||
}
|
||||
evidence := retentionEvidence{ArchivedSegments: report.RawSegmentsArchived, ArchivedBytes: report.RawBytesArchived, RemovedSegments: report.RawSegmentsRemoved, RemovedBytes: report.RawBytesRemoved, ProjectionRowsRemoved: report.ProjectedObservationsRemoved, ColdQueryRows: len(result.Rows)}
|
||||
if evidence.ArchivedSegments != 4 || evidence.RemovedSegments != 1 || evidence.ProjectionRowsRemoved != 4 || evidence.ColdQueryRows != 1 {
|
||||
return retentionEvidence{}, fmt.Errorf("retention lifecycle mismatch: %+v", evidence)
|
||||
}
|
||||
return evidence, nil
|
||||
}
|
||||
|
||||
func capacityBudget(rows int) query.Budget {
|
||||
return query.Budget{MaxDuration: 10 * time.Second, MaxRows: rows, MaxScannedBytes: 64 << 30, MaxMemoryBytes: 1 << 30}
|
||||
}
|
||||
|
||||
func percentile(values []time.Duration, fraction float64) time.Duration {
|
||||
copyOfValues := append([]time.Duration(nil), values...)
|
||||
sort.Slice(copyOfValues, func(left, right int) bool { return copyOfValues[left] < copyOfValues[right] })
|
||||
index := int(math.Ceil(float64(len(copyOfValues))*fraction)) - 1
|
||||
if index < 0 {
|
||||
index = 0
|
||||
}
|
||||
return copyOfValues[index]
|
||||
}
|
||||
|
||||
func milliseconds(value time.Duration) float64 { return float64(value) / float64(time.Millisecond) }
|
||||
|
||||
func cgroupLimits() (float64, int64, error) {
|
||||
cpuBody, err := os.ReadFile("/sys/fs/cgroup/cpu.max")
|
||||
if err != nil {
|
||||
return 0, 0, errors.New("read cgroup CPU limit")
|
||||
}
|
||||
parts := strings.Fields(string(cpuBody))
|
||||
if len(parts) != 2 || parts[0] == "max" {
|
||||
return 0, 0, errors.New("cgroup CPU quota is not finite")
|
||||
}
|
||||
quota, quotaErr := strconv.ParseFloat(parts[0], 64)
|
||||
period, periodErr := strconv.ParseFloat(parts[1], 64)
|
||||
memoryBody, memoryErr := os.ReadFile("/sys/fs/cgroup/memory.max")
|
||||
memory, parseMemoryErr := strconv.ParseInt(strings.TrimSpace(string(memoryBody)), 10, 64)
|
||||
if quotaErr != nil || periodErr != nil || period <= 0 || memoryErr != nil || parseMemoryErr != nil {
|
||||
return 0, 0, errors.New("cgroup resource limit is invalid")
|
||||
}
|
||||
return quota / period, memory, nil
|
||||
}
|
||||
|
||||
func maximumRSS() int64 {
|
||||
var usage syscall.Rusage
|
||||
if syscall.Getrusage(syscall.RUSAGE_SELF, &usage) != nil {
|
||||
return 0
|
||||
}
|
||||
return usage.Maxrss * 1024
|
||||
}
|
||||
|
||||
func directoryBytes(root string) (int64, error) {
|
||||
var total int64
|
||||
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if entry.Type()&os.ModeSymlink != 0 {
|
||||
return errors.New("capacity workspace contains a symlink")
|
||||
}
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil || !info.Mode().IsRegular() {
|
||||
return errors.New("capacity workspace contains a non-regular file")
|
||||
}
|
||||
if info.Size() > math.MaxInt64-total {
|
||||
return errors.New("capacity workspace size overflow")
|
||||
}
|
||||
total += info.Size()
|
||||
return nil
|
||||
})
|
||||
return total, err
|
||||
}
|
||||
|
||||
func measureStorage(root, primaryOrganizationID string) (storageBreakdown, error) {
|
||||
var report storageBreakdown
|
||||
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
|
||||
if walkErr != nil {
|
||||
return walkErr
|
||||
}
|
||||
if entry.Type()&os.ModeSymlink != 0 {
|
||||
return errors.New("capacity dataset contains a symlink")
|
||||
}
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil || !info.Mode().IsRegular() {
|
||||
return errors.New("capacity dataset contains a non-regular file")
|
||||
}
|
||||
relative, err := filepath.Rel(root, path)
|
||||
if err != nil || relative == "." || strings.HasPrefix(relative, ".."+string(os.PathSeparator)) {
|
||||
return errors.New("capacity dataset path is invalid")
|
||||
}
|
||||
class := &report.Other
|
||||
switch {
|
||||
case relative == "control.sqlite" || strings.HasPrefix(relative, "control.sqlite-"):
|
||||
class = &report.Control
|
||||
case strings.HasPrefix(relative, "raw"+string(os.PathSeparator)) || strings.HasPrefix(relative, "cold"+string(os.PathSeparator)):
|
||||
class = &report.Raw
|
||||
case strings.HasPrefix(relative, "organizations"+string(os.PathSeparator)):
|
||||
class = &report.Projection
|
||||
}
|
||||
if info.Size() > math.MaxInt64-class.Bytes || info.Size() > math.MaxInt64-report.Total.Bytes {
|
||||
return errors.New("capacity storage class size overflow")
|
||||
}
|
||||
class.Files++
|
||||
class.Bytes += info.Size()
|
||||
report.Total.Files++
|
||||
report.Total.Bytes += info.Size()
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return storageBreakdown{}, err
|
||||
}
|
||||
projectionPath := filepath.Join(root, "organizations", primaryOrganizationID, "projection.sqlite")
|
||||
report.SQLiteBytes, report.SQLitePageClasses, err = sqliteObjectBytes(projectionPath)
|
||||
if err != nil {
|
||||
return storageBreakdown{}, err
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
|
||||
func sqliteObjectBytes(path string) (map[string]int64, sqlitePageBreakdown, error) {
|
||||
dsn := (&url.URL{Scheme: "file", Path: path, RawQuery: "mode=ro"}).String()
|
||||
db, err := sql.Open("sqlite", dsn)
|
||||
if err != nil {
|
||||
return nil, sqlitePageBreakdown{}, errors.New("open capacity projection diagnostics")
|
||||
}
|
||||
defer db.Close()
|
||||
db.SetMaxOpenConns(1)
|
||||
rows, err := db.Query(`SELECT d.name,COALESCE(m.type,'internal'),COALESCE(SUM(d.pgsize),0) FROM dbstat AS d LEFT JOIN sqlite_schema AS m ON m.name=d.name GROUP BY d.name,m.type ORDER BY d.name`)
|
||||
if err != nil {
|
||||
return nil, sqlitePageBreakdown{}, errors.New("read capacity projection page accounting")
|
||||
}
|
||||
defer rows.Close()
|
||||
report := map[string]int64{}
|
||||
var classes sqlitePageBreakdown
|
||||
for rows.Next() {
|
||||
var name, objectType string
|
||||
var bytes int64
|
||||
if err = rows.Scan(&name, &objectType, &bytes); err != nil || name == "" || bytes < 0 {
|
||||
return nil, sqlitePageBreakdown{}, errors.New("read capacity projection page accounting")
|
||||
}
|
||||
report[name] = bytes
|
||||
if bytes > math.MaxInt64-classes.Total {
|
||||
return nil, sqlitePageBreakdown{}, errors.New("capacity projection page accounting overflow")
|
||||
}
|
||||
classes.Total += bytes
|
||||
switch objectType {
|
||||
case "table":
|
||||
classes.Tables += bytes
|
||||
case "index":
|
||||
classes.Indexes += bytes
|
||||
default:
|
||||
classes.Internal += bytes
|
||||
}
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, sqlitePageBreakdown{}, errors.New("read capacity projection page accounting")
|
||||
}
|
||||
if len(report) == 0 {
|
||||
return nil, sqlitePageBreakdown{}, errors.New("capacity projection page accounting is empty")
|
||||
}
|
||||
if classes.Total != classes.Tables+classes.Indexes+classes.Internal {
|
||||
return nil, sqlitePageBreakdown{}, errors.New("capacity projection page accounting total mismatch")
|
||||
}
|
||||
return report, classes, nil
|
||||
}
|
||||
Reference in New Issue
Block a user