Skip to content

ENG-534: Per-request instance sizing#4

Merged
clawhavenapp merged 2 commits into
mainfrom
commonzenpython/eng-534-per-request-instance-sizing
Jul 7, 2026
Merged

ENG-534: Per-request instance sizing#4
clawhavenapp merged 2 commits into
mainfrom
commonzenpython/eng-534-per-request-instance-sizing

Conversation

@czpython

@czpython czpython commented Jul 6, 2026

Copy link
Copy Markdown
Owner

Was stacked on #1; #1 merged, now rebased onto main (base retargeted).

What

POST /hosts accepts two optional sizing fields: instance_type (provider-native, e.g. AWS t3.xlarge, Hetzner cx33) and disk_gb (root disk in GB). The values are stamped on the host row (NULL = provider's configured default), forwarded from provision() to create_vm, and reflected by GET /hosts/{id}. Alembic migration 0002_host_sizing adds the two nullable columns.

Provider support is declared on the adapter via supports_instance_type / supports_disk_gb class vars (default False on VMProvider):

  • AWS honors both — instance_type → EC2 InstanceType, disk_gb → root EBS volume size.
  • Hetzner honors instance_type (maps onto its native server_type); disk_gb is rejected because Hetzner fixes root disk size by server type.
  • exe.dev / docker expose no sizing; any sized request targeting them is rejected.

HostService.create_host checks the flags right after resolving the provider, so unsupported sizing returns a clear 400 (same mapping as unknown provider) before any host row or VM exists — no silent fallback, nothing for the janitor to reap.

Pool interaction (per the pool-per-provider work in #1): warm hosts are provisioned at the provider's default size, so sizing joins image/env in the customization check — a sized request never claims a default-sized pool member and always provisions fresh. Noted in docs/architecture.md.

Why raw provider-native instance_type instead of a portable class

The ticket left portable-class (standard/large) vs raw-type-passthrough open. This PR goes provider-native, as explicit named fields (instance_type + disk_gb), not a free-form bag:

  • A portable class needs a per-provider class→type mapping table — a new config axis with no consumer today. The mapping can be layered on later without breaking the raw field.
  • Sizing composes with per-request provider selection (already in HostCreate.provider): a caller pinning a size knows which provider it's sizing for, so provider-native names are unambiguous, and the provider itself validates them (a bad type fails provisioning loudly).
  • crabbox exposes raw --type passthrough alongside --class, so the raw field is the primitive either way.

Unsupported sizing is refused via declarative class-var flags + one service-side check rather than a capability mix-in or a runtime create_vm error: there is no new method surface to mix in (sizing rides create_vm params), and rejecting inside create_vm would fire after the row exists, turning a caller mistake into a 502 + janitor cleanup instead of the required 4xx.

Acceptance criteria

  • POST /hosts with no sizing fields behaves exactly as today (provider default; instance_type/disk_gb surface as null)
  • POST /hosts with an instance_type provisions a host of that size on AWS; GET /hosts/{id} reflects it
  • Unsupported sizing on a provider → clear 4xx (400 with the provider and field named), not a silent fallback
  • A sized request never claims a default-sized warm pool host

Verification

uv run ruff check, uv run ruff format --check, uv run pyright, uv run pytest (290 passed) all green; alembic upgrade head / downgrade -1 / upgrade head cycle verified on a scratch DB. Codex review against the stack base: clean, zero findings.

Rebase note: rebased onto the base branch's review-fix commit 68719e5 (shuffled per-tick top-up order in pool._maintain + regression test). Clean replay, no conflicts; this PR doesn't touch pool._maintain, and the pool maintainer creates hosts without sizing, so there's no interaction with the shuffle fix. Full gate re-run green on the rebased tree (ruff, ruff format --check, pyright, pytest 291 passed).

🤖 Generated with Claude Code

@czpython
czpython force-pushed the commonzenpython/eng-534-per-request-instance-sizing branch from e590048 to a38e518 Compare July 7, 2026 00:42
@clawhavenapp
clawhavenapp changed the base branch from commonzenpython/eng-525-pool-per-provider-provider-aware-warm-pool to main July 7, 2026 17:38
POST /hosts accepts optional instance_type (provider-native, e.g.
t3.xlarge / cx33) and disk_gb. The values are stamped on the host row
(NULL = provider default), forwarded from provision() to create_vm, and
reflected by GET. Providers declare support via supports_instance_type /
supports_disk_gb class vars: AWS honors both, Hetzner maps instance_type
to its server type, exe.dev and docker expose no sizing — the service
rejects unsupported sizing with a 400 before any row or VM exists.

A sized request is never pool-eligible: warm hosts are provisioned at
the provider's default size, so sizing joins image/env in the
customization check that forces fresh provisioning.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@czpython
czpython force-pushed the commonzenpython/eng-534-per-request-instance-sizing branch from a38e518 to c54f937 Compare July 7, 2026 17:41
The base VMProvider already declares supports_instance_type/supports_disk_gb
as ClassVar[bool]; pyright inherits that classification on override, so
restating ClassVar on each subclass added nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@clawhavenapp
clawhavenapp merged commit 1227cdc into main Jul 7, 2026
6 checks passed
@clawhavenapp
clawhavenapp deleted the commonzenpython/eng-534-per-request-instance-sizing branch July 7, 2026 18:00
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.

2 participants