Skip to content

MotherDuck: migrate to the shared benchmark runner - #2702

Open
jtigani wants to merge 4 commits into
ClickHouse:mainfrom
jtigani:motherduck-shared-runner
Open

jtigani wants to merge 4 commits into
ClickHouse:mainfrom
jtigani:motherduck-shared-runner

Conversation

@jtigani

@jtigani jtigani commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

What

Migrates the motherduck/ entry from its self-contained driver
(benchmark.py / run.sh / query.py) to the shared
lib/benchmark-common.sh runner that the rest of the catalog now uses.

Core migration

Replaces the old driver with the lifecycle scripts the shared runner expects —
install, start, stop, check, load, query, data-size,
template.json, and a thin benchmark.sh. The SQL (create.sql,
queries.sql, the load CTAS) is unchanged.

MotherDuck is a managed, serverless service, so the driver settings are:

  • BENCH_DOWNLOAD_SCRIPT="" — nothing is downloaded to the client; load
    CTAS's directly from the ClickBench parquet on S3 into MotherDuck.
  • BENCH_RESTARTABLE=no — the engine is remote; there's no local daemon to
    stop/start, and it's inherently "no-cold".
  • BENCH_DURABLE=yes — data persists across client restarts.
  • Concurrent-QPS off by default (each try is a fresh connection, so it would
    measure connection setup rather than throughput).

MotherDuck-specific helpers

These are isolated to motherduck/ and are not part of the shared contract:

  • set-instance-size — change the account's read_write Duckling (compute) size
    via the MotherDuck REST API (it can't be set via SQL). Shuts the duckling down
    cleanly, PUTs the new size, waits for stabilization, verifies via
    md_live_duckling_size().
  • run-all-sizes.sh — run the full benchmark once per Duckling size
    (largest-first; giga opt-in), emitting a committable result per size.
  • make-result — convert a per-size log into results/<date>/<size>.json
    labeled Motherduck: <size>. The shared cloud-init derives machine from the
    client EC2 instance type, which is meaningless for a remote service, so we
    commit results directly (as the existing MotherDuck results do).

Notes for reviewers

  • Code-only PR; fresh result JSONs can be submitted separately.
  • Happy to split the MotherDuck-specific helpers into a follow-up if you'd
    prefer the bare migration to land on its own.
  • The shared cloud-init has no secret-forwarding for motherduck_token and
    labels machine by the client EC2 type, so running this under the official
    AWS automation would need a dedicated workflow (like clickhouse-cloud.yml).
    Flagging for guidance.

🤖 Generated with Claude Code

jtigani and others added 4 commits October 18, 2026 11:23
Replace the self-contained driver (benchmark.py / run.sh / query.py) with
the lifecycle scripts the shared driver (lib/benchmark-common.sh) expects:
install, start, stop, check, load, query, data-size, plus template.json
and a thin benchmark.sh that sets the BENCH_* knobs and exec's the lib.

MotherDuck is a managed, serverless cloud service, so:
  - BENCH_DOWNLOAD_SCRIPT="" — load CTAS's directly from S3 into the cloud;
    nothing is fetched onto the client VM.
  - BENCH_RESTARTABLE=no — the engine is remote; restarting the local client
    and dropping its page cache does nothing to MotherDuck (inherently
    "no-cold"), and would otherwise burn bench_wait_stopped's 60s timeout.
  - BENCH_DURABLE=yes — data persists in the cloud across client restarts.
  - BENCH_CONCURRENT_DURATION=0 — each query opens a fresh md: connection,
    so the QPS test would mostly measure connection setup; off by default.

load/data-size/query reuse the original SQL (create.sql, the epoch_ms /
EventDate coercions, pragma_database_size -> bytes). query passes its program
via `python3 -c` so stdin stays free for the piped query, and uses
con.execute() to tolerate the trailing ";" on each queries.sql line.

Verified end to end against MotherDuck: full load (99,997,497 rows), data
size ~17.2 GiB, and all 43 queries run with 0 failures.

Known follow-ups (outside this directory, needed for the official AWS
automation): cloud-init labels results with the client EC2 instance type
rather than the MotherDuck warehouse size, and has no generic secret
forwarding for motherduck_token — both want a dedicated workflow like
clickhouse-cloud.yml.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Duckling (compute) size can't be set via SQL or the md: connection string; it
is an account setting changed through the MotherDuck REST API. Add helpers:

  set-instance-size <pulse|standard|jumbo|mega|giga>
    GET the current config, PUT read_write.instance_size (preserving the
    read_scaling block the API requires), then poll md_live_duckling_size()
    until the new size reports live. That function flips before the compute is
    stable, so it then settles for STABILIZE_SECONDS (default 120; >=2 min is
    required). Username is auto-discovered via MD_USER_INFO().

  run-all-sizes.sh [size ...]
    Install the client once, then for each size: set-instance-size -> run the
    full benchmark -> make-result. Runs proceed largest size first; the default
    set is mega/jumbo/standard/pulse (giga excluded by default since some trial
    accounts lack it; opt in with INCLUDE_GIGA=1 or by naming it). A size the
    account can't use is skipped, not fatal. RESTORE_SIZE resets at the end.

  make-result <size> [log] [date]
    Convert a per-size log into results/<date>/<size>.json labeled
    "Motherduck: <size>". The shared cloud-init derives a result's machine from
    the client EC2 type, which is wrong for a remote service, so we commit
    results directly (as the existing MotherDuck results do) rather than rely on
    the sink path.

Also add the "DuckDB derivative" tag to template.json to match the published
MotherDuck entry.

Verified end to end on EC2 (c6a.4xlarge, us-east-1) against MotherDuck: the full
largest-first sweep ran mega/jumbo/standard/pulse with 43/43 queries and no
failures, set-instance-size performed real PUTs across all four sizes, and the
generated results JSONs pass validate-results.py and match the format/numbers of
the published PR ClickHouse#2319 (load times within a few percent).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
set-instance-size now, in order: issues SHUTDOWN NORMAL so the current duckling
shuts down cleanly, PUTs the new read_write size, waits SETTLE_SECONDS
(default 120, min 120) WITHOUT querying MotherDuck so the duckling can finish
shutting down on its own, then double-checks md_live_duckling_size() reports the
new size (polling up to SET_SIZE_TIMEOUT if propagation lags).

This replaces the previous "PUT then poll, then stabilize" approach. The fixed
2-minute wait is required — md_live_duckling_size() flips before the compute is
settled — and nothing may query MotherDuck during it or the old duckling is kept
alive and never recycles. duckdb is now a hard requirement (used for SHUTDOWN
NORMAL and the size check); the STABILIZE_SECONDS knob is renamed SETTLE_SECONDS.

SHUTDOWN NORMAL returns (True,) and leaves the connection usable; verified live
along with username discovery and the GET. The PUT is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Each query try opens a fresh md: connection. Connect with
config={"motherduck_attach_mode": "single"} so it attaches only the database the
benchmark uses instead of enumerating the whole workspace catalog on every
connect. Verified via the config dict (the connection-string form is silently
ignored); queries still return correct results.

Context: the geomean comparison vs PR ClickHouse#2319 showed the migrated entry carries a
small, systematic per-query overhead from the one-connection-per-try model
(worst on fast queries / cold runs). Single-attach trims the catalog work of
that per-try connect. A local A/B was inconclusive (in-process connection
pooling + cross-country latency mask the fresh-process cost); the real effect
should be measured on the co-located EC2 client.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jtigani
jtigani requested a deployment to benchmark-approval October 20, 2026 03:03 — with GitHub Actions Waiting

@jtigani jtigani left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I (or, Claude actually) migrated MotherDuck to use the new shared runner infrastructure. Wanted to make sure we following the correct protocols. Also included a couple of scripts that can be used to run all motherduck production instance sizes. Happy to remove those if is preferable to keep the repo clean.

@jtigani
jtigani marked this pull request as ready for review October 20, 2026 03:14

This branch is waiting to be deployed

1 waiting deployment
benchmark-approval — 97b99a50 Waiting Oct 20, 2026 by jtigani via launch #854
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant