Skip to content

SRE-819: Add stub atlas subcommand to exercise the deployment pipeline#9044

Merged
TimDiekmann merged 2 commits into
mainfrom
t/sre-819-stub-graph-atlas-subcommand-for-end-to-end-infra-testing
Jul 16, 2026
Merged

SRE-819: Add stub atlas subcommand to exercise the deployment pipeline#9044
TimDiekmann merged 2 commits into
mainfrom
t/sre-819-stub-graph-atlas-subcommand-for-end-to-end-infra-testing

Conversation

@TimDiekmann

Copy link
Copy Markdown
Member

🌟 What is the purpose of this PR?

Adds a minimal atlas subcommand to the hash-graph binary so the full deployment chain for the new atlas service can be exercised before the real SALT Atlas implementation lands: image build → staging redeploy → ECS container healthcheck → Node-API reachability → production promotion test.

The stub serves only GET /status; the real implementation replaces the router while keeping the subcommand name, address configuration, and healthcheck wiring.

🔗 Related links

🚫 Blocked by

  • Nothing.

🔍 What does this change?

  • apps/hash-graph/src/subcommand/atlas.rs: new subcommand modeled on type-fetcher — clap args (HASH_ATLAS_HOST/HASH_ATLAS_PORT, default 127.0.0.1:4003), an axum router with GET /status, the shared HealthcheckArgs/wait_healthcheck wiring (so /hash-graph atlas --healthcheck works, which the ECS task definition probes), and the same lifecycle/Ctrl-C handling as the other server subcommands
  • apps/hash-graph/src/subcommand/mod.rs: registers the Atlas subcommand (service name "Atlas" for telemetry)
  • infra/compose/compose.yml: atlas service (graph image, command: atlas, port 4003, healthcheck) under the hash profile, plus HASH_ATLAS_HOST/HASH_ATLAS_PORT on the api service — the local mirror of the production wiring

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

  • The compose atlas service pulls ghcr.io/hashintel/hash/graph — the subcommand exists in that image only after this PR merges and the next graph image publishes, so --profile hash fails on the atlas service in that short window.

🐾 Next steps

  • The real SALT Atlas implementation replaces the stub router (DB access, S3-backed blobs, /status semantics)
  • Infra raises the staging desired count once the image containing the subcommand is deployed

🛡 What tests cover this?

  • subcommand::atlas::tests::status_endpoint_reports_healthy — boots the stub router on an ephemeral port and asserts the healthcheck passes against /status

❓ How to test this?

  1. cargo run -p hash-graph -- atlas (binds 127.0.0.1:4003)
  2. curl -I http://localhost:4003/status → 200
  3. cargo run -p hash-graph -- atlas --healthcheck → exits 0 while the server runs

@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview, Comment Jul 16, 2026 3:58pm
petrinaut Ready Ready Preview, Comment Jul 16, 2026 3:58pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Jul 16, 2026 3:58pm

@cursor

cursor Bot commented Jul 16, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
New isolated stub service with no auth, data, or changes to existing graph/API behavior beyond optional compose env for the API.

Overview
Adds a new hash-graph atlas CLI subcommand as a placeholder SALT Atlas service so staging/production can run image build, ECS healthchecks, and API wiring before the real implementation exists.

The stub is an Axum server on HASH_ATLAS_HOST / HASH_ATLAS_PORT (default 127.0.0.1:4003) with only GET /status, shared --healthcheck / wait_healthcheck behavior (HEAD to /status), and the same ServerLifecycle / Ctrl-C shutdown pattern as other server subcommands. A unit test asserts the healthcheck passes against the stub router.

infra/compose/compose.yml adds an atlas service under the hash profile (same graph image, command: atlas, port 4003, /hash-graph atlas --healthcheck) and sets HASH_ATLAS_HOST / HASH_ATLAS_PORT on api so local compose mirrors production reachability.

Reviewed by Cursor Bugbot for commit 28e538d. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions github-actions Bot added area/apps > hash* Affects HASH (a `hash-*` app) area/infra Relates to version control, CI, CD or IaC (area) type/eng > backend Owned by the @backend team area/apps area/apps > hash-graph labels Jul 16, 2026
@TimDiekmann
TimDiekmann requested review from a team July 16, 2026 15:47
@TimDiekmann
TimDiekmann enabled auto-merge July 16, 2026 15:47
Comment thread infra/compose/compose.yml
@TimDiekmann
TimDiekmann added this pull request to the merge queue Jul 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Benchmark results

@rust/hash-graph-benches – Integrations

policy_resolution_large

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 2002 $$30.1 \mathrm{ms} \pm 221 \mathrm{μs}\left({\color{gray}3.83 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$3.73 \mathrm{ms} \pm 27.5 \mathrm{μs}\left({\color{red}6.89 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 1002 $$14.8 \mathrm{ms} \pm 97.4 \mathrm{μs}\left({\color{red}7.03 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 3314 $$46.2 \mathrm{ms} \pm 429 \mathrm{μs}\left({\color{gray}3.00 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$15.8 \mathrm{ms} \pm 126 \mathrm{μs}\left({\color{gray}4.09 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 1527 $$26.7 \mathrm{ms} \pm 257 \mathrm{μs}\left({\color{red}5.08 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 2078 $$31.7 \mathrm{ms} \pm 273 \mathrm{μs}\left({\color{gray}4.92 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$4.18 \mathrm{ms} \pm 25.3 \mathrm{μs}\left({\color{red}9.31 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 1033 $$16.2 \mathrm{ms} \pm 112 \mathrm{μs}\left({\color{red}7.10 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_medium

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 102 $$3.94 \mathrm{ms} \pm 29.2 \mathrm{μs}\left({\color{gray}2.84 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$3.12 \mathrm{ms} \pm 19.6 \mathrm{μs}\left({\color{gray}3.29 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 52 $$3.55 \mathrm{ms} \pm 23.0 \mathrm{μs}\left({\color{gray}3.42 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 269 $$5.69 \mathrm{ms} \pm 50.8 \mathrm{μs}\left({\color{red}6.85 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$4.09 \mathrm{ms} \pm 30.8 \mathrm{μs}\left({\color{red}8.90 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 108 $$4.83 \mathrm{ms} \pm 32.1 \mathrm{μs}\left({\color{red}13.5 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 133 $$4.74 \mathrm{ms} \pm 33.1 \mathrm{μs}\left({\color{gray}3.24 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$3.61 \mathrm{ms} \pm 27.9 \mathrm{μs}\left({\color{gray}3.70 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 63 $$4.26 \mathrm{ms} \pm 31.1 \mathrm{μs}\left({\color{gray}-1.632 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_none

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 2 $$2.72 \mathrm{ms} \pm 16.0 \mathrm{μs}\left({\color{gray}-0.451 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.65 \mathrm{ms} \pm 12.9 \mathrm{μs}\left({\color{gray}-2.937 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 2 $$2.80 \mathrm{ms} \pm 17.2 \mathrm{μs}\left({\color{gray}-1.055 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 8 $$3.09 \mathrm{ms} \pm 22.8 \mathrm{μs}\left({\color{gray}-1.270 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$2.85 \mathrm{ms} \pm 14.0 \mathrm{μs}\left({\color{gray}-1.870 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 3 $$3.20 \mathrm{ms} \pm 25.2 \mathrm{μs}\left({\color{gray}-0.600 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_small

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 52 $$3.15 \mathrm{ms} \pm 22.0 \mathrm{μs}\left({\color{gray}-2.976 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.89 \mathrm{ms} \pm 21.7 \mathrm{μs}\left({\color{gray}-1.789 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 26 $$3.11 \mathrm{ms} \pm 19.8 \mathrm{μs}\left({\color{gray}0.932 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 94 $$3.62 \mathrm{ms} \pm 31.9 \mathrm{μs}\left({\color{gray}-4.988 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$3.17 \mathrm{ms} \pm 24.7 \mathrm{μs}\left({\color{gray}-2.237 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 27 $$3.41 \mathrm{ms} \pm 24.2 \mathrm{μs}\left({\color{gray}-2.600 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 66 $$3.50 \mathrm{ms} \pm 27.7 \mathrm{μs}\left({\color{gray}-3.174 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$3.09 \mathrm{ms} \pm 20.2 \mathrm{μs}\left({\color{gray}-3.375 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 29 $$3.39 \mathrm{ms} \pm 21.3 \mathrm{μs}\left({\color{gray}-2.389 \mathrm{\%}}\right) $$ Flame Graph

read_scaling_complete

Function Value Mean Flame graphs
entity_by_id;one_depth 1 entities $$34.4 \mathrm{ms} \pm 193 \mathrm{μs}\left({\color{gray}2.80 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 10 entities $$75.2 \mathrm{ms} \pm 374 \mathrm{μs}\left({\color{gray}-0.306 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 25 entities $$39.6 \mathrm{ms} \pm 357 \mathrm{μs}\left({\color{gray}4.19 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 5 entities $$42.3 \mathrm{ms} \pm 187 \mathrm{μs}\left({\color{gray}3.45 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 50 entities $$46.5 \mathrm{ms} \pm 248 \mathrm{μs}\left({\color{gray}2.05 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 1 entities $$36.8 \mathrm{ms} \pm 268 \mathrm{μs}\left({\color{gray}1.58 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 10 entities $$425 \mathrm{ms} \pm 1.32 \mathrm{ms}\left({\color{gray}-2.315 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 25 entities $$95.2 \mathrm{ms} \pm 562 \mathrm{μs}\left({\color{gray}-2.640 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 5 entities $$83.0 \mathrm{ms} \pm 370 \mathrm{μs}\left({\color{gray}-2.647 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 50 entities $$318 \mathrm{ms} \pm 1.34 \mathrm{ms}\left({\color{red}9.74 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 1 entities $$11.6 \mathrm{ms} \pm 90.9 \mathrm{μs}\left({\color{gray}-0.606 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 10 entities $$11.9 \mathrm{ms} \pm 73.1 \mathrm{μs}\left({\color{gray}1.93 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 25 entities $$12.0 \mathrm{ms} \pm 84.0 \mathrm{μs}\left({\color{gray}0.691 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 5 entities $$12.0 \mathrm{ms} \pm 77.6 \mathrm{μs}\left({\color{gray}3.23 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 50 entities $$12.0 \mathrm{ms} \pm 69.5 \mathrm{μs}\left({\color{gray}3.19 \mathrm{\%}}\right) $$ Flame Graph

read_scaling_linkless

Function Value Mean Flame graphs
entity_by_id 1 entities $$11.9 \mathrm{ms} \pm 92.5 \mathrm{μs}\left({\color{gray}1.28 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 10 entities $$12.0 \mathrm{ms} \pm 71.3 \mathrm{μs}\left({\color{gray}4.06 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 100 entities $$12.1 \mathrm{ms} \pm 75.4 \mathrm{μs}\left({\color{red}6.52 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 1000 entities $$12.4 \mathrm{ms} \pm 105 \mathrm{μs}\left({\color{red}7.91 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 10000 entities $$12.6 \mathrm{ms} \pm 85.1 \mathrm{μs}\left({\color{red}7.65 \mathrm{\%}}\right) $$ Flame Graph

representative_read_entity

Function Value Mean Flame graphs
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/block/v/1 $$12.6 \mathrm{ms} \pm 74.3 \mathrm{μs}\left({\color{red}6.50 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1 $$12.7 \mathrm{ms} \pm 80.8 \mathrm{μs}\left({\color{red}8.82 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1 $$12.5 \mathrm{ms} \pm 76.6 \mathrm{μs}\left({\color{red}7.53 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1 $$12.4 \mathrm{ms} \pm 91.9 \mathrm{μs}\left({\color{red}5.36 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2 $$12.4 \mathrm{ms} \pm 83.6 \mathrm{μs}\left({\color{red}5.36 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1 $$12.6 \mathrm{ms} \pm 84.3 \mathrm{μs}\left({\color{red}7.52 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1 $$12.6 \mathrm{ms} \pm 93.8 \mathrm{μs}\left({\color{red}6.27 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1 $$12.5 \mathrm{ms} \pm 81.5 \mathrm{μs}\left({\color{red}6.89 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1 $$12.8 \mathrm{ms} \pm 103 \mathrm{μs}\left({\color{red}7.06 \mathrm{\%}}\right) $$ Flame Graph

representative_read_entity_type

Function Value Mean Flame graphs
get_entity_type_by_id Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba $$9.09 \mathrm{ms} \pm 52.8 \mathrm{μs}\left({\color{red}6.92 \mathrm{\%}}\right) $$ Flame Graph

representative_read_multiple_entities

Function Value Mean Flame graphs
entity_by_property traversal_paths=0 0 $$68.0 \mathrm{ms} \pm 471 \mathrm{μs}\left({\color{red}7.38 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=255 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true $$124 \mathrm{ms} \pm 774 \mathrm{μs}\left({\color{red}7.43 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false $$76.1 \mathrm{ms} \pm 599 \mathrm{μs}\left({\color{red}8.20 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true $$86.0 \mathrm{ms} \pm 582 \mathrm{μs}\left({\color{red}9.19 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true $$97.3 \mathrm{ms} \pm 731 \mathrm{μs}\left({\color{red}10.9 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true $$103 \mathrm{ms} \pm 583 \mathrm{μs}\left({\color{red}9.43 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=0 0 $$52.6 \mathrm{ms} \pm 455 \mathrm{μs}\left({\color{red}7.86 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=255 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true $$80.7 \mathrm{ms} \pm 541 \mathrm{μs}\left({\color{red}6.56 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false $$59.8 \mathrm{ms} \pm 317 \mathrm{μs}\left({\color{red}8.16 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true $$69.4 \mathrm{ms} \pm 288 \mathrm{μs}\left({\color{red}8.88 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true $$71.3 \mathrm{ms} \pm 386 \mathrm{μs}\left({\color{red}8.00 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true $$71.3 \mathrm{ms} \pm 454 \mathrm{μs}\left({\color{red}8.23 \mathrm{\%}}\right) $$

scenarios

Function Value Mean Flame graphs
full_test query-limited $$117 \mathrm{ms} \pm 591 \mathrm{μs}\left({\color{lightgreen}-8.245 \mathrm{\%}}\right) $$ Flame Graph
full_test query-unlimited $$131 \mathrm{ms} \pm 942 \mathrm{μs}\left({\color{lightgreen}-5.169 \mathrm{\%}}\right) $$ Flame Graph
linked_queries query-limited $$19.4 \mathrm{ms} \pm 137 \mathrm{μs}\left({\color{lightgreen}-5.593 \mathrm{\%}}\right) $$ Flame Graph
linked_queries query-unlimited $$532 \mathrm{ms} \pm 1.15 \mathrm{ms}\left({\color{gray}-0.211 \mathrm{\%}}\right) $$ Flame Graph

Merged via the queue into main with commit 8f97376 Jul 16, 2026
59 checks passed
@TimDiekmann
TimDiekmann deleted the t/sre-819-stub-graph-atlas-subcommand-for-end-to-end-infra-testing branch July 16, 2026 17:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/apps > hash* Affects HASH (a `hash-*` app) area/apps > hash-graph area/apps area/infra Relates to version control, CI, CD or IaC (area) type/eng > backend Owned by the @backend team

Development

Successfully merging this pull request may close these issues.

2 participants