From f3e00fe9db15d50941e3b7100f549396af9f6776 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Patel Date: Sun, 9 Aug 2026 20:21:44 +0530 Subject: [PATCH 1/6] Add adaptive AIMD auto-throttle: workers steer rate by target latency when a task opts in --- internal/engine/adaptive.go | 55 +++++++++++++++++++++++++++++++++ internal/engine/leaserunner.go | 46 ++++++++++++++++++++++----- internal/engine/runner.go | 49 ++++++++++++++++++++++++++--- internal/models/task.go | 4 +++ internal/store/task.go | 14 ++++----- migrations/011_task_adaptive.go | 17 ++++++++++ migrations/all.go | 1 + 7 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 internal/engine/adaptive.go create mode 100644 migrations/011_task_adaptive.go diff --git a/internal/engine/adaptive.go b/internal/engine/adaptive.go new file mode 100644 index 0000000..d515f74 --- /dev/null +++ b/internal/engine/adaptive.go @@ -0,0 +1,55 @@ +package engine + +import ( + "context" + "database/sql" + "time" + + "marathon/internal/engine/throttle" +) + +// adaptiveDefaultCeiling bounds an adaptive run whose task has no explicit rate +// (rate_per_sec == 0). Without a ceiling the controller would ramp unbounded. +const adaptiveDefaultCeiling = 50_000 + +// probeInterval limits how often we sample target latency — at most once per +// this window — so a fast batch loop doesn't flood the DB with probes. +const probeInterval = 250 * time.Millisecond + +// adaptiveCeiling turns a task rate into the controller's upper bound: the +// task's rate when set, otherwise a sane default. +func adaptiveCeiling(taskRate int) float64 { + if taskRate > 0 { + return float64(taskRate) + } + + return adaptiveDefaultCeiling +} + +// newAdaptive builds an AIMD controller starting at the ceiling and ramping +// within [min, ceiling] based on observed target latency. +func newAdaptive(taskRate int) *throttle.AIMD { + ceiling := adaptiveCeiling(taskRate) + + return throttle.NewAIMD(throttle.AIMDConfig{ + Start: ceiling, // begin optimistic; back off on the first painful sample + Ceiling: ceiling, + }) +} + +// probeQuery is the round-trip used to sample target latency. A test seam lets +// integration tests substitute a deliberately slow query to exercise back-off. +var probeQuery = "SELECT 1" + +// probeLatency times a trivial round-trip to the target. This is the health +// signal the controller reacts to: when the DB is loaded, even `SELECT 1` slows. +func probeLatency(ctx context.Context, db *sql.DB) time.Duration { + start := time.Now() + + if err := db.QueryRowContext(ctx, probeQuery).Scan(new(int)); err != nil { + // A failed/slow probe is itself a strong "back off" signal. + return time.Second + } + + return time.Since(start) +} diff --git a/internal/engine/leaserunner.go b/internal/engine/leaserunner.go index e0ced7f..2e3060b 100644 --- a/internal/engine/leaserunner.go +++ b/internal/engine/leaserunner.go @@ -33,9 +33,11 @@ type LeaseWorker struct { queue *coord.Queue task models.Task runID int64 - bucket *throttle.Bucket - lastRate int - emit ProgressFunc + bucket *throttle.Bucket + adaptive *throttle.AIMD // non-nil when the task opts into auto-throttle + lastRate int + lastProbe time.Time + emit ProgressFunc tasks store.TaskStore runs store.RunStore @@ -43,7 +45,7 @@ type LeaseWorker struct { } func NewLeaseWorker(id string, db store.DB, log store.Logger, queue *coord.Queue, task models.Task, runID int64) *LeaseWorker { - return &LeaseWorker{ + w := &LeaseWorker{ id: id, db: db, log: log, @@ -53,19 +55,47 @@ func NewLeaseWorker(id string, db store.DB, log store.Logger, queue *coord.Queue bucket: throttle.New(task.RatePerSec), lastRate: task.RatePerSec, } + + if task.Adaptive { + w.adaptive = newAdaptive(task.RatePerSec) + } + + return w } // refreshRate picks up a live throttle change (from the dashboard speed slider) -// by re-reading the task's rate and adjusting this worker's bucket. Called once -// per lease, so already-running fleet workers respond to speed changes. +// by re-reading the task's rate. Called once per lease, so already-running fleet +// workers respond to speed changes. In adaptive mode the rate is the ceiling and +// the controller still governs the actual rate; otherwise it sets the bucket. func (w *LeaseWorker) refreshRate(ctx context.Context) { rate, err := w.tasks.GetRate(ctx, w.db, w.task.ID) if err != nil || rate == w.lastRate { return } - w.bucket.SetRate(rate) w.lastRate = rate + + if w.adaptive != nil { + w.adaptive.SetCeiling(adaptiveCeiling(rate)) + return + } + + w.bucket.SetRate(rate) +} + +// tune samples target latency (rate-limited) and steers the bucket rate via the +// AIMD controller. No-op unless the task opts into adaptive throttling. +func (w *LeaseWorker) tune(ctx context.Context, target *sql.DB) { + if w.adaptive == nil { + return + } + + if !w.lastProbe.IsZero() && time.Since(w.lastProbe) < probeInterval { + return + } + + w.lastProbe = time.Now() + w.bucket.SetRate(int(w.adaptive.Observe(probeLatency(ctx, target)))) } func (w *LeaseWorker) WithProgress(fn ProgressFunc) *LeaseWorker { @@ -170,6 +200,8 @@ func (w *LeaseWorker) processLease(ctx context.Context, target *sql.DB, lease co return ctx.Err() } + w.tune(ctx, target) + if err := w.bucket.Take(ctx, w.task.BatchSize); err != nil { return err } diff --git a/internal/engine/runner.go b/internal/engine/runner.go index 97022b6..a17b2d7 100644 --- a/internal/engine/runner.go +++ b/internal/engine/runner.go @@ -8,6 +8,7 @@ import ( "database/sql" "errors" "fmt" + "sync/atomic" "time" _ "github.com/jackc/pgx/v5/stdlib" // target-DB driver @@ -34,8 +35,10 @@ type Runner struct { task models.Task runID int64 - bucket *throttle.Bucket - emit ProgressFunc + bucket *throttle.Bucket + adaptive *throttle.AIMD // non-nil when the task opts into auto-throttle + ceiling atomic.Int64 // live ceiling (rows/sec) for adaptive mode + emit ProgressFunc batchIdx int // crashHook, when set, is called after a batch's operation commits on the @@ -66,13 +69,20 @@ type Progress struct { } func NewRunner(db store.DB, log store.Logger, task models.Task, runID int64) *Runner { - return &Runner{ + r := &Runner{ db: db, log: log, task: task, runID: runID, bucket: throttle.New(task.RatePerSec), } + + if task.Adaptive { + r.adaptive = newAdaptive(task.RatePerSec) + r.ceiling.Store(int64(adaptiveCeiling(task.RatePerSec))) + } + + return r } // WithProgress registers a live-progress emitter. @@ -82,8 +92,16 @@ func (r *Runner) WithProgress(fn ProgressFunc) *Runner { } // SetRate changes the throttle rate (rows/sec, 0 = unlimited) of a running -// runner, live. Used by the dashboard speed slider. +// runner, live. Used by the dashboard speed slider. In adaptive mode the slider +// sets the ceiling; the controller still governs the actual rate below it. The +// ceiling is applied in the runner goroutine (via an atomic) so the AIMD is +// never mutated concurrently. func (r *Runner) SetRate(rowsPerSec int) { + if r.adaptive != nil { + r.ceiling.Store(int64(adaptiveCeiling(rowsPerSec))) + return + } + r.bucket.SetRate(rowsPerSec) } @@ -146,6 +164,8 @@ func (r *Runner) run(ctx context.Context) error { } } + var lastProbe time.Time + for { state, err := r.runs.GetState(ctx, r.db, r.runID) if err != nil { @@ -169,6 +189,8 @@ func (r *Runner) run(ctx context.Context) error { return nil // terminal state reached elsewhere; stop quietly } + r.tune(ctx, target, &lastProbe) + if err := r.bucket.Take(ctx, r.task.BatchSize); err != nil { return err } @@ -215,6 +237,25 @@ func (r *Runner) run(ctx context.Context) error { } } +// tune, in adaptive mode, samples target latency (rate-limited by probeInterval) +// and steers the token-bucket rate via the AIMD controller. All AIMD access is +// in this goroutine; SetRate only stores the ceiling atomically. +func (r *Runner) tune(ctx context.Context, target *sql.DB, lastProbe *time.Time) { + if r.adaptive == nil { + return + } + + if !lastProbe.IsZero() && time.Since(*lastProbe) < probeInterval { + return + } + + *lastProbe = time.Now() + + r.adaptive.SetCeiling(float64(r.ceiling.Load())) + rate := r.adaptive.Observe(probeLatency(ctx, target)) + r.bucket.SetRate(int(rate)) +} + // emitSnapshot publishes the run's current state to live subscribers. func (r *Runner) emitSnapshot(ctx context.Context, batchRows int) { if r.emit == nil { diff --git a/internal/models/task.go b/internal/models/task.go index 043a854..c998002 100644 --- a/internal/models/task.go +++ b/internal/models/task.go @@ -35,6 +35,10 @@ type Task struct { OperationSQL string `json:"operation_sql"` OperationURL string `json:"operation_url"` RatePerSec int `json:"rate_per_sec"` + // Adaptive enables the AIMD auto-throttle: the worker watches the target + // database's latency and adjusts the rate automatically, backing off when + // the DB is under load. rate_per_sec then acts as the ceiling. + Adaptive bool `json:"adaptive"` // ScheduleSeconds > 0 makes the task recurring: the scheduler starts a new // run every N seconds (skipping ticks while a run is still active). 0 = manual. ScheduleSeconds int `json:"schedule_seconds"` diff --git a/internal/store/task.go b/internal/store/task.go index d0fd15d..aaef902 100644 --- a/internal/store/task.go +++ b/internal/store/task.go @@ -15,11 +15,11 @@ type TaskStore struct{} func (TaskStore) Create(ctx context.Context, db DB, t *models.Task) error { err := db.QueryRowContext(ctx, ` INSERT INTO tasks (name, connection_id, target_driver, target_dsn, source_table, cursor_column, row_filter, - batch_size, operation_type, operation_sql, operation_url, rate_per_sec, schedule_seconds) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + batch_size, operation_type, operation_sql, operation_url, rate_per_sec, schedule_seconds, adaptive) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING id, created_at`, t.Name, t.ConnectionID, t.TargetDriver, t.TargetDSN, t.SourceTable, t.CursorColumn, t.RowFilter, - t.BatchSize, t.OperationType, t.OperationSQL, t.OperationURL, t.RatePerSec, t.ScheduleSeconds, + t.BatchSize, t.OperationType, t.OperationSQL, t.OperationURL, t.RatePerSec, t.ScheduleSeconds, t.Adaptive, ).Scan(&t.ID, &t.CreatedAt) if err != nil { return fmt.Errorf("create task: %w", err) @@ -55,10 +55,10 @@ func (TaskStore) Get(ctx context.Context, db DB, id int64) (models.Task, error) err := db.QueryRowContext(ctx, ` SELECT id, name, connection_id, target_driver, target_dsn, source_table, cursor_column, row_filter, - batch_size, operation_type, operation_sql, operation_url, rate_per_sec, schedule_seconds, created_at + batch_size, operation_type, operation_sql, operation_url, rate_per_sec, schedule_seconds, adaptive, created_at FROM tasks WHERE id = $1`, id, ).Scan(&t.ID, &t.Name, &t.ConnectionID, &t.TargetDriver, &t.TargetDSN, &t.SourceTable, &t.CursorColumn, &t.RowFilter, - &t.BatchSize, &t.OperationType, &t.OperationSQL, &t.OperationURL, &t.RatePerSec, &t.ScheduleSeconds, &t.CreatedAt) + &t.BatchSize, &t.OperationType, &t.OperationSQL, &t.OperationURL, &t.RatePerSec, &t.ScheduleSeconds, &t.Adaptive, &t.CreatedAt) if err != nil { return models.Task{}, fmt.Errorf("get task %d: %w", id, err) } @@ -69,7 +69,7 @@ func (TaskStore) Get(ctx context.Context, db DB, id int64) (models.Task, error) func (TaskStore) List(ctx context.Context, db DB) ([]models.Task, error) { rows, err := db.QueryContext(ctx, ` SELECT id, name, connection_id, target_driver, target_dsn, source_table, cursor_column, row_filter, - batch_size, operation_type, operation_sql, operation_url, rate_per_sec, schedule_seconds, created_at + batch_size, operation_type, operation_sql, operation_url, rate_per_sec, schedule_seconds, adaptive, created_at FROM tasks ORDER BY id DESC`) if err != nil { return nil, fmt.Errorf("list tasks: %w", err) @@ -83,7 +83,7 @@ func (TaskStore) List(ctx context.Context, db DB) ([]models.Task, error) { if err := rows.Scan(&t.ID, &t.Name, &t.ConnectionID, &t.TargetDriver, &t.TargetDSN, &t.SourceTable, &t.CursorColumn, &t.RowFilter, &t.BatchSize, &t.OperationType, &t.OperationSQL, &t.OperationURL, - &t.RatePerSec, &t.ScheduleSeconds, &t.CreatedAt); err != nil { + &t.RatePerSec, &t.ScheduleSeconds, &t.Adaptive, &t.CreatedAt); err != nil { return nil, fmt.Errorf("scan task: %w", err) } diff --git a/migrations/011_task_adaptive.go b/migrations/011_task_adaptive.go new file mode 100644 index 0000000..3f77d6e --- /dev/null +++ b/migrations/011_task_adaptive.go @@ -0,0 +1,17 @@ +package migrations + +import "gofr.dev/pkg/gofr/migration" + +// Adds the adaptive auto-throttle opt-in: when true, the worker steers the rate +// by observed target-DB latency, using rate_per_sec as the ceiling. +func addAdaptive() migration.Migrate { + return migration.Migrate{ + UP: func(d migration.Datasource) error { + _, err := d.SQL.Exec(` + ALTER TABLE tasks + ADD COLUMN IF NOT EXISTS adaptive BOOLEAN NOT NULL DEFAULT false`) + + return err + }, + } +} diff --git a/migrations/all.go b/migrations/all.go index ca02ad8..0de43ef 100644 --- a/migrations/all.go +++ b/migrations/all.go @@ -16,5 +16,6 @@ func All() map[int64]migration.Migrate { 8: addTargetDriver(), 9: addSchedule(), 10: addTaskConnection(), + 11: addAdaptive(), } } From 22ff7c1f8314de25c09d0fb39faae4c098c1a0c8 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Patel Date: Sun, 9 Aug 2026 20:21:44 +0530 Subject: [PATCH 2/6] Test adaptive throttle: run completes and backs off under injected latency --- internal/engine/adaptive_test.go | 96 +++++++++++++++++++++++++++++ internal/engine/integration_test.go | 3 +- 2 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 internal/engine/adaptive_test.go diff --git a/internal/engine/adaptive_test.go b/internal/engine/adaptive_test.go new file mode 100644 index 0000000..9d812a9 --- /dev/null +++ b/internal/engine/adaptive_test.go @@ -0,0 +1,96 @@ +package engine + +import ( + "context" + "testing" + + "marathon/internal/models" + "marathon/internal/store" +) + +func TestAdaptiveCeiling(t *testing.T) { + if got := adaptiveCeiling(8000); got != 8000 { + t.Errorf("adaptiveCeiling(8000) = %v, want 8000", got) + } + + if got := adaptiveCeiling(0); got != adaptiveDefaultCeiling { + t.Errorf("adaptiveCeiling(0) = %v, want default %d", got, adaptiveDefaultCeiling) + } +} + +// An adaptive run completes correctly against a healthy DB — the feedback loop +// must never break correctness (every row applied exactly once). +func TestIntegration_AdaptiveRunCompletes(t *testing.T) { + control, target := openOrSkip(t) + controlSchema(t, control) + + const n = 10000 + seedTarget(t, target, "items", n, "") + + task := &models.Task{ + Name: "adaptive-ok", TargetDSN: dsn("MARATHON_TEST_TARGET_DSN", "postgres://demo:demo@localhost:5434/demo?sslmode=disable"), + SourceTable: "items", CursorColumn: "id", BatchSize: 1000, + RatePerSec: 20000, Adaptive: true, + OperationSQL: "UPDATE items SET applied = applied + 1 WHERE id >= $1 AND id <= $2", + } + newTask(t, control, task) + + run, err := (store.RunStore{}).Create(context.Background(), control, task.ID) + if err != nil { + t.Fatal(err) + } + + r := NewRunner(control, nopLogger{}, *task, run.ID) + if r.adaptive == nil { + t.Fatal("adaptive controller should be set for an adaptive task") + } + + r.Run(context.Background()) + + assertState(t, control, run.ID, models.RunSucceeded) + assertCount(t, target, "SELECT count(*) FROM items WHERE applied <> 1", 0, "rows not applied exactly once") +} + +// Under injected latency the controller backs the rate off well below its +// ceiling — the point of the auto-throttle. +func TestIntegration_AdaptiveBacksOffUnderLatency(t *testing.T) { + control, target := openOrSkip(t) + controlSchema(t, control) + + const n = 4000 + seedTarget(t, target, "items", n, "") + + // Make each probe take ~80ms (> the 50ms high watermark) so every sample + // tells the controller to decrease. + orig := probeQuery + probeQuery = "SELECT 1 FROM pg_sleep(0.08)" + defer func() { probeQuery = orig }() + + const ceiling = 20000 + task := &models.Task{ + Name: "adaptive-backoff", TargetDSN: dsn("MARATHON_TEST_TARGET_DSN", "postgres://demo:demo@localhost:5434/demo?sslmode=disable"), + SourceTable: "items", CursorColumn: "id", BatchSize: 500, + RatePerSec: ceiling, Adaptive: true, + OperationSQL: "UPDATE items SET applied = applied + 1 WHERE id >= $1 AND id <= $2", + } + newTask(t, control, task) + + run, err := (store.RunStore{}).Create(context.Background(), control, task.ID) + if err != nil { + t.Fatal(err) + } + + r := NewRunner(control, nopLogger{}, *task, run.ID) + r.Run(context.Background()) + + assertState(t, control, run.ID, models.RunSucceeded) + + // The controller should have driven the rate far below the ceiling in + // response to the sustained high latency. + if got := r.adaptive.Rate(); got >= ceiling { + t.Errorf("adaptive rate = %.0f, expected well below ceiling %d after sustained latency", got, ceiling) + } + + // Correctness is unaffected by throttling. + assertCount(t, target, "SELECT count(*) FROM items WHERE applied <> 1", 0, "rows not applied exactly once") +} diff --git a/internal/engine/integration_test.go b/internal/engine/integration_test.go index bd5e772..3b0f18c 100644 --- a/internal/engine/integration_test.go +++ b/internal/engine/integration_test.go @@ -99,7 +99,8 @@ func controlSchema(t *testing.T, db *sql.DB) { ADD COLUMN IF NOT EXISTS operation_url TEXT NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS target_driver TEXT NOT NULL DEFAULT 'postgres', ADD COLUMN IF NOT EXISTS schedule_seconds INT NOT NULL DEFAULT 0, - ADD COLUMN IF NOT EXISTS connection_id BIGINT REFERENCES connections(id)`); err != nil { + ADD COLUMN IF NOT EXISTS connection_id BIGINT REFERENCES connections(id), + ADD COLUMN IF NOT EXISTS adaptive BOOLEAN NOT NULL DEFAULT false`); err != nil { t.Fatalf("control alter: %v", err) } From 24847bcbba714827fa7488e2b5f739afa1314a5f Mon Sep 17 00:00:00 2001 From: Nitin Kumar Patel Date: Sun, 9 Aug 2026 20:21:44 +0530 Subject: [PATCH 3/6] Dashboard: adaptive auto-throttle toggle and AUTO indicator --- web/src/pages/RunLive.tsx | 7 +++++-- web/src/pages/TaskWizard.tsx | 14 ++++++++++++-- web/src/pages/Tasks.tsx | 2 +- web/src/styles.css | 6 ++++++ web/src/types.ts | 1 + 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/web/src/pages/RunLive.tsx b/web/src/pages/RunLive.tsx index b6fce88..46994f7 100644 --- a/web/src/pages/RunLive.tsx +++ b/web/src/pages/RunLive.tsx @@ -21,6 +21,7 @@ export function RunLive({ runId, onBack }: { runId: number; onBack: () => void } const [notice, setNotice] = useState(""); const [speed, setSpeed] = useState(null); const [speedMax, setSpeedMax] = useState(100000); + const [adaptive, setAdaptive] = useState(false); const last = useRef<{ t: number; processed: number } | null>(null); const speedTimer = useRef | undefined>(undefined); @@ -31,6 +32,7 @@ export function RunLive({ runId, onBack }: { runId: number; onBack: () => void } .then((t) => { setSpeed(t.rate_per_sec); setSpeedMax(Math.max(100000, (t.rate_per_sec || 0) * 2)); + setAdaptive(t.adaptive); }) .catch(() => {}); // eslint-disable-next-line react-hooks/exhaustive-deps @@ -142,8 +144,8 @@ export function RunLive({ runId, onBack }: { runId: number; onBack: () => void } {active && speed !== null && (
- Throttle - {speed === 0 ? "Max (unthrottled)" : `${fmt(speed)} rows/s`} + Throttle {adaptive && AUTO} + {speed === 0 ? (adaptive ? "Auto (no ceiling)" : "Max (unthrottled)") : `${adaptive ? "ceiling " : ""}${fmt(speed)} rows/s`}
void } onChange={(e) => onSpeed(Number(e.target.value))} aria-label="Throttle rate" /> + {adaptive &&
Auto-throttle adjusts the live rate by the target's latency; the slider sets the ceiling.
}
)} diff --git a/web/src/pages/TaskWizard.tsx b/web/src/pages/TaskWizard.tsx index 2203037..d4077e4 100644 --- a/web/src/pages/TaskWizard.tsx +++ b/web/src/pages/TaskWizard.tsx @@ -22,6 +22,7 @@ type Form = { operation_url: string; batch_size: number; rate_per_sec: number; + adaptive: boolean; schedule_seconds: number; }; @@ -38,6 +39,7 @@ const initial: Form = { operation_url: "", batch_size: 2000, rate_per_sec: 10000, + adaptive: false, schedule_seconds: 0, }; @@ -300,9 +302,9 @@ export function TaskWizard({ set("batch_size", +e.target.value)} />
- + set("rate_per_sec", +e.target.value)} /> -
0 = unthrottled.
+
{f.adaptive ? "Upper bound; auto-throttle stays below it." : "0 = unthrottled."}
@@ -310,6 +312,14 @@ export function TaskWizard({
0 = manual only.
+ + diff --git a/web/src/pages/Tasks.tsx b/web/src/pages/Tasks.tsx index e46a2c1..153d747 100644 --- a/web/src/pages/Tasks.tsx +++ b/web/src/pages/Tasks.tsx @@ -78,7 +78,7 @@ export function Tasks({ onOpenRun, onNew }: { onOpenRun: (runId: number) => void {t.target_driver || "postgres"} {t.operation_type === "http" ? "http callback" : "sql"} {t.source_table} · batch {fmt(t.batch_size)} - · {t.rate_per_sec ? `${fmt(t.rate_per_sec)}/s` : "max rate"} + · {t.adaptive ? `auto ≤ ${t.rate_per_sec ? fmt(t.rate_per_sec) : "50k"}/s` : t.rate_per_sec ? `${fmt(t.rate_per_sec)}/s` : "max rate"} {t.schedule_seconds > 0 && · ⟳ every {t.schedule_seconds}s} diff --git a/web/src/styles.css b/web/src/styles.css index 912c0f7..c4c4dc5 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -187,6 +187,11 @@ a { color: var(--accent); text-decoration: none; } .row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; } .row3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; } +.toggle-row { display: flex; gap: 11px; align-items: flex-start; padding: 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--paper-2); cursor: pointer; } +.toggle-row input { margin-top: 3px; width: 16px; height: 16px; accent-color: var(--accent); flex: none; cursor: pointer; } +.toggle-row strong { font-size: 13.5px; } +.toggle-row .hint { font-size: 12.5px; color: var(--ink-2); } + .seg { display: inline-flex; background: var(--paper-2); border: 1px solid var(--line-2); border-radius: 9px; padding: 3px; gap: 3px; } .seg button { border: 0; background: transparent; color: var(--ink-2); font: inherit; font-weight: 600; font-size: 13px; padding: 7px 15px; border-radius: 6px; cursor: pointer; transition: all .14s; } .seg button.on { background: var(--card); color: var(--accent); box-shadow: 0 1px 2px #0002; } @@ -286,6 +291,7 @@ a { color: var(--accent); text-decoration: none; } border: 3px solid var(--card); box-shadow: 0 1px 4px #0003; cursor: pointer; } .speed input[type="range"]:focus-visible { box-shadow: 0 0 0 3px var(--accent-ring); } +.auto-chip { font-size: 9px; font-weight: 800; letter-spacing: 0.6px; padding: 1px 6px; border-radius: 5px; background: var(--good-weak); color: var(--good); margin-left: 6px; vertical-align: 1px; } .controls { display: flex; gap: 10px; margin-top: 20px; flex-wrap: wrap; align-items: center; } .done-note { color: var(--good); font-weight: 550; font-size: 13.5px; } diff --git a/web/src/types.ts b/web/src/types.ts index 659d71a..eb913fe 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -19,6 +19,7 @@ export interface Task { operation_sql: string; operation_url: string; rate_per_sec: number; + adaptive: boolean; schedule_seconds: number; created_at: string; } From 028212b3f858dc2107526565c04fafd37e222213 Mon Sep 17 00:00:00 2001 From: Nitin Kumar Patel Date: Sun, 9 Aug 2026 20:21:44 +0530 Subject: [PATCH 4/6] Add adaptive run-console screenshot --- docs/screenshots/run-adaptive.png | Bin 0 -> 69650 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/screenshots/run-adaptive.png diff --git a/docs/screenshots/run-adaptive.png b/docs/screenshots/run-adaptive.png new file mode 100644 index 0000000000000000000000000000000000000000..eea766346bfc1c2a05c86438c6d809d3faad197c GIT binary patch literal 69650 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 YN&7oE>Y48AU`8h)Dl1a(`knWG15=kkKL7v# literal 0 HcmV?d00001 From eb9ecbcc37c57cd21adc75ba222f11181cba331e Mon Sep 17 00:00:00 2001 From: Nitin Kumar Patel Date: Sun, 9 Aug 2026 23:13:21 +0530 Subject: [PATCH 5/6] Ignore stray null websocket frames so the run console can't crash --- web/src/api.ts | 7 ++++++- web/src/pages/RunLive.tsx | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/web/src/api.ts b/web/src/api.ts index 8bb0693..f9ce113 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -77,7 +77,12 @@ export function liveSocket( ws.onopen = () => ws.send(JSON.stringify({ run_id: runId })); ws.onmessage = (ev) => { try { - onMessage(JSON.parse(ev.data)); + const p = JSON.parse(ev.data); + // The server can emit a `null` frame (e.g. an empty handshake result); + // only forward well-formed progress objects. + if (p && typeof p === "object" && "run_id" in p) { + onMessage(p as import("./types").Progress); + } } catch { /* ignore malformed frame */ } diff --git a/web/src/pages/RunLive.tsx b/web/src/pages/RunLive.tsx index 46994f7..3b8b6ac 100644 --- a/web/src/pages/RunLive.tsx +++ b/web/src/pages/RunLive.tsx @@ -49,6 +49,7 @@ export function RunLive({ runId, onBack }: { runId: number; onBack: () => void } useEffect(() => { api.getRun(runId).then(setRun).catch(() => {}); const ws = liveSocket(runId, (p: Progress) => { + if (!p) return; setRun((prev) => ({ ...(prev as Run), ...p, id: p.run_id })); const now = Date.now(); if (last.current) { From 1312d8ed9526072dabf8894d26f06eaa648cd7fd Mon Sep 17 00:00:00 2001 From: Nitin Kumar Patel Date: Sun, 9 Aug 2026 23:17:20 +0530 Subject: [PATCH 6/6] Fix dark-mode colors: button hover, status badge borders, pill borders, row/scrollbar hover were hardcoded light --- web/src/styles.css | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/web/src/styles.css b/web/src/styles.css index c4c4dc5..76f1332 100644 --- a/web/src/styles.css +++ b/web/src/styles.css @@ -22,9 +22,13 @@ --accent-ring: #1d54d633; --accent-border: #d3e0fb; - --good: #12805c; --good-weak: #e4f3ec; - --warn: #a8690f; --warn-weak: #f9efdb; - --crit: #c23548; --crit-weak: #fbe7ea; + --good: #12805c; --good-weak: #e4f3ec; --good-border: #bfe3d1; + --warn: #a8690f; --warn-weak: #f9efdb; --warn-border: #e7d3a8; + --crit: #c23548; --crit-weak: #fbe7ea; --crit-border: #eec3c9; + + --hover-bg: #fafbfd; /* row / subtle hover surface */ + --btn-hover-bg: #f5f7fa; /* default button hover */ + --btn-hover-border: #c2cad6; --code-bg: #eef2f8; --code-fg: #263043; --code-border: #dde3ee; --grid: #00000010; @@ -54,9 +58,13 @@ --accent-ring: #5488ff44; --accent-border: #24365c; - --good: #34c78d; --good-weak: #10281f; - --warn: #e0a951; --warn-weak: #2a2113; - --crit: #ef6b7a; --crit-weak: #2c1720; + --good: #34c78d; --good-weak: #10281f; --good-border: #24503b; + --warn: #e0a951; --warn-weak: #2a2113; --warn-border: #5a4a24; + --crit: #ef6b7a; --crit-weak: #2c1720; --crit-border: #5c2a34; + + --hover-bg: #1a2130; /* row / subtle hover surface */ + --btn-hover-bg: #1c2434; /* default button hover */ + --btn-hover-border: #3a4557; --code-bg: #0a0e16; --code-fg: #cdd7ea; --code-border: #212836; --grid: #ffffff12; @@ -81,7 +89,7 @@ a { color: var(--accent); text-decoration: none; } ::-webkit-scrollbar { width: 10px; height: 10px; } ::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 20px; border: 3px solid var(--paper); } -::-webkit-scrollbar-thumb:hover { background: #c2cad6; } +::-webkit-scrollbar-thumb:hover { background: var(--btn-hover-border); } /* ---------- shell -------------------------------------------------------- */ .app { display: grid; grid-template-columns: 232px 1fr; min-height: 100%; } @@ -157,13 +165,15 @@ a { color: var(--accent); text-decoration: none; } background: var(--card); color: var(--ink); font: inherit; font-weight: 550; font-size: 13.5px; cursor: pointer; transition: all .14s; white-space: nowrap; } -.btn:hover { border-color: #c2cad6; background: #fafbfc; } +.btn:hover { border-color: var(--btn-hover-border); background: var(--btn-hover-bg); } .btn:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-ring); } .btn:disabled { opacity: .5; cursor: not-allowed; } .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; box-shadow: 0 6px 16px -8px var(--accent-ring); } .btn.primary:hover { background: var(--accent-press); border-color: var(--accent-press); } -.btn.warn { background: var(--warn-weak); border-color: #e7d3a8; color: var(--warn); } -.btn.danger { background: var(--crit-weak); border-color: #eec3c9; color: var(--crit); } +.btn.warn { background: var(--warn-weak); border-color: var(--warn-border); color: var(--warn); } +.btn.warn:hover { border-color: var(--warn); } +.btn.danger { background: var(--crit-weak); border-color: var(--crit-border); color: var(--crit); } +.btn.danger:hover { border-color: var(--crit); } .btn.ghost { background: transparent; border-color: transparent; color: var(--ink-2); } .btn.ghost:hover { background: var(--paper-2); color: var(--ink); } .btn.lg { padding: 12px 22px; font-size: 15px; } @@ -211,25 +221,25 @@ a { color: var(--accent); text-decoration: none; } /* ---------- badges / pills ---------------------------------------------- */ .badge { display: inline-flex; align-items: center; gap: 7px; padding: 5px 12px; border-radius: 999px; font-size: 11px; font-weight: 700; letter-spacing: .5px; text-transform: uppercase; border: 1px solid transparent; } .badge .dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; } -.badge.running { background: var(--accent-weak); color: var(--accent); border-color: #cfddfb; } +.badge.running { background: var(--accent-weak); color: var(--accent); border-color: var(--accent-border); } .badge.running .dot { animation: pulse 1.5s infinite; } -.badge.paused { background: var(--warn-weak); color: var(--warn); border-color: #ecd9b0; } -.badge.succeeded { background: var(--good-weak); color: var(--good); border-color: #c2e2d3; } -.badge.failed, .badge.killed { background: var(--crit-weak); color: var(--crit); border-color: #efc7ce; } +.badge.paused { background: var(--warn-weak); color: var(--warn); border-color: var(--warn-border); } +.badge.succeeded { background: var(--good-weak); color: var(--good); border-color: var(--good-border); } +.badge.failed, .badge.killed { background: var(--crit-weak); color: var(--crit); border-color: var(--crit-border); } .badge.queued { background: var(--paper-2); color: var(--ink-2); border-color: var(--line-2); } @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .3; } } @media (prefers-reduced-motion: reduce) { .badge .dot { animation: none; } } .pill { font-family: var(--mono); font-size: 11px; padding: 2px 8px; border-radius: 6px; background: var(--paper-2); color: var(--ink-2); border: 1px solid var(--line); } -.pill.pg { color: var(--accent); background: var(--accent-weak); border-color: #d3e0fb; } -.pill.mysql { color: var(--warn); background: var(--warn-weak); border-color: #ecd9b0; } +.pill.pg { color: var(--accent); background: var(--accent-weak); border-color: var(--accent-border); } +.pill.mysql { color: var(--warn); background: var(--warn-weak); border-color: var(--warn-border); } .pill.pending { color: var(--warn); } .pill.resolved { color: var(--good); } /* ---------- task list ---------------------------------------------------- */ .tasklist { display: flex; flex-direction: column; } .taskrow { display: flex; align-items: center; gap: 16px; padding: 18px 26px; border-top: 1px solid var(--line); transition: background .14s; } .taskrow:first-child { border-top: 0; } -.taskrow:hover { background: #fafbfd; } +.taskrow:hover { background: var(--hover-bg); } .taskrow .tname { font-weight: 640; font-size: 15px; letter-spacing: -0.2px; } .taskrow .tmeta { color: var(--faint); font-size: 12px; margin-top: 4px; display: flex; gap: 8px; flex-wrap: wrap; align-items: center; } .taskrow .spacer { flex: 1; }