Skip to content

BE-815: atlas: serve more than one generation at a time - #9642

Draft
indietyp wants to merge 74 commits into
bm/be-831-hashql-support-type-parameters-in-the-id-macrofrom
bm/be-815-atlas-serve-more-than-one-generation-at-a-time
Draft

BE-815: atlas: serve more than one generation at a time#9642
indietyp wants to merge 74 commits into
bm/be-831-hashql-support-type-parameters-in-the-id-macrofrom
bm/be-815-atlas-serve-more-than-one-generation-at-a-time

Conversation

@indietyp

@indietyp indietyp commented Sep 10, 2026

Copy link
Copy Markdown
Member

🌟 What is the purpose of this PR?

Serve more than one fit generation per process and switch between them without a restart. Before this change, the server opened one generation at start and held it for the process lifetime. A swap meant a restart, and a restart dropped every in-flight session, since a token carried one epoch and the new epoch invalidated it. We also had no way to coordinate a switch with S3, since the logic would need to run while the server is still running, then shut itself down completely via AWS, and then recreate it for the new root. This has obvious downsides, as it couples the implementation to the chosen provider. Having generations be native allows for two things:

  1. We're able to gradually promote generations, meaning that viewers who view it right now don't actually know a switch is happening
  2. We have a process that can coordinate the aforementioned switch operation.

What made this project larger is that the prior implementation was heavily tied to a "god class", aka a single class does everything, and only after its inception delta-insertion was grafted on top of it (which made OOMs possible). This changes it so everything now revolves around delta insertion, and everything outside of two classes is oblivious to it. Every operation now operates over a Scene which contains a World (the fact as we know it right now - aka space) and an Epoch, which tracks delta accumulated stores. When accessing a world, you must always specify the point in time (via Epoch) that pins the revision; this allows us to stop advancing prematurely.

As this is larger in quantity (as I also took care to clean up the previously messy code), the following things are to be considered during review:

  • A request sees one coherent pair of generation and delta publication for its whole life, taken under a single read lock at admission.
  • Promotion never loses the last good publication. An opening failure or a feed that ends keeps what stood in service and retries at the poll cadence.
  • Admission enforces retention on its own, independent of cleanup. The registry refuses a request for an expired generation even when its directory still exists.
  • Shutdown stops every feed before joining any, and removal never waits on a request that still holds a world.
  • The routes, the problem types and the saltile media type stay as they were.
  • Everything else is either just a straight 1:1 port or copy from the previous implementation.

Nice benefit of the new architecture: hot tiles are now twice as fast.

🔍 What does this change?

This replaces the serving layer (libs/@local/graph/atlas/src/serve/).

Both serve and cli were/are the messier ones that would need to be revisited anyway to allow for maintainable code down the line; the cli is now the only one left (BE-804).

flowchart LR
    C[current pointer] -->|poll| M[GenerationManager]
    M -->|open| W[World + Delta feed]
    M -->|promote / retire / unlink| R[UniverseRegistry]
    Q[request] -->|observe| R
    R -->|"Observation: present + requested"| S[Scene]
    S --> D[document: tile · edges · locate · translate · manifest · current]
    D -->|saltile / cbor / json| Q
Loading
  • runtime/: GenerationManager owns generation execution (open, promote, retire, unlink, shutdown) on a poll loop the host runs as its own task. UniverseRegistry is the read side: observe(requested) returns the present generation and the requested one together under one lock, with retention timed from the replacement's promotion.
  • world/: the opened, immutable artifacts of one generation (layout, topology, node index, ontology), checked for a shared node domain at open. Universe pairs a World with the delta Epoch captured for the request.
  • delta/: the live overlay on a world, versioned by revision and identified per lifetime by a DeltaId drawn at open. A reopened generation gets a new id, so tokens from the previous lifetime refuse uniformly. Placement, projector, history and feed each have their own module.
  • scene.rs and document/: one captured Scene (world, epoch, visibility mask, delivery schedule) feeds every document type, so every response comes from one snapshot. Documents encode through document/codec into the saltile envelope, which now records its own content type for the API to return.
  • visibility/ and api/authorization/: one visibility-cache budget across all generations. Entries key on the requested generation and its DeltaId, so a retired generation's misses cannot resolve a fresh scope. Authority tokens verify the actor, the requested generation and the DeltaId. Renewal on the same filter keeps its offset, and a changed filter rebinds.
  • hydrate/: the store boundary behind resolver traits (OntologyResolver, TypeUrlResolver) with SQL statement snapshots, so the tree pins what the process asks Postgres.
  • file/generation/: a generation lock with cooperative removal, plus staging and scratch directories. The original metadata bytes stay on disk so a generation's content-derived identity survives a reopen.
  • CLI and host: hash-graph atlas serve gains --generation-poll-interval, --unlink-expired-generations and --delta-minimum-projection-interval, each with its HASH_GRAPH_ATLAS_* variable. ServeCommand::run returns Serving, whose into_parts hands the host the router and the maintenance future as two values, and apps/hash-graph registers that future on its lifecycle before binding the listener.
  • Frontend decoder (apps/hash-frontend/…/atlas-decode): the intern-table check accepts unique entries in index order rather than requiring bytewise sort, matching the relaxed writer.

⚠️ Breaking Changes & Known Issues

  • Translate responses no longer honour the spelling of an entity id by the user, and instead enforce the consistent spelling used everywhere else.
  • A retained generation whose feed has stopped or is unable to start will only see the fit-time points
  • Directory removal after expiry is opt-in (--unlink-expired-generations) and off by default.
  • Instrumentation and metrics are largely missing (BE-842)

🛡 What tests cover this?

The tests in the serve directory 🙂

❓ How to test this?

Run the server, create a new generation (either by copying or otherwise), then wait. You should see the changeover.

will be adding a demo if I don't forget

@indietyp
indietyp added this pull request to stack #9506 September 10, 2026 10:56
@vercel

vercel Bot commented Sep 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
hash Ready Ready Preview Sep 11, 2026 7:16pm UTC
petrinaut Ready Ready Preview Sep 11, 2026 7:16pm UTC
petrinaut-docs Ready Ready Preview Sep 11, 2026 7:16pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
hashdotdesign-tokens Ignored Ignored Preview Sep 11, 2026 7:16pm UTC

Request Review

@github-actions github-actions Bot added area/deps Relates to third-party dependencies (area) area/apps > hash* Affects HASH (a `hash-*` app) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) type/eng > frontend Owned by the @frontend team type/eng > backend Owned by the @backend team area/tests New or updated tests area/apps area/apps > hash-graph labels Sep 10, 2026
Comment thread libs/@local/graph/atlas/src/file/generation/staging.rs
Comment thread libs/@local/graph/atlas/src/file/generation/staging.rs
Comment thread libs/@local/graph/atlas/src/file/generation/scratch.rs
@codecov

codecov Bot commented Sep 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 6.25000% with 15 lines in your changes missing coverage. Please review.
✅ Project coverage is 61.14%. Comparing base (7619239) to head (0038630).

Files with missing lines Patch % Lines
apps/hash-graph/src/subcommand/atlas.rs 6.25% 15 Missing ⚠️
Additional details and impacted files
@@                                     Coverage Diff                                      @@
##           bm/be-831-hashql-support-type-parameters-in-the-id-macro    #9642      +/-   ##
============================================================================================
- Coverage                                                     65.89%   61.14%   -4.76%     
============================================================================================
  Files                                                          1887     1352     -535     
  Lines                                                        198443   140468   -57975     
  Branches                                                       8248     6365    -1883     
============================================================================================
- Hits                                                         130773    85885   -44888     
+ Misses                                                        66140    53513   -12627     
+ Partials                                                       1530     1070     -460     
Flag Coverage Δ
apps.hash-ai-worker-py ?
apps.hash-ai-worker-ts 1.99% <ø> (ø)
apps.hash-api 15.35% <ø> (ø)
apps.hash-graph 12.44% <6.25%> (-0.10%) ⬇️
backend-integration-tests ?
blockprotocol.type-system 38.15% <ø> (ø)
deer ?
error-stack ?
local.claude-hooks 0.00% <ø> (ø)
local.harpc-client 51.49% <ø> (ø)
local.hash-backend-utils 3.27% <ø> (ø)
local.hash-graph-sdk 10.02% <ø> (ø)
local.hash-isomorphic-utils 12.22% <ø> (ø)
local.hash-subgraph ?
rust.antsi 2.36% <ø> (ø)
rust.deer ?
rust.error-stack 90.81% <ø> (ø)
rust.harpc-codec 84.70% <ø> (ø)
rust.harpc-net 96.21% <ø> (-0.02%) ⬇️
rust.harpc-tower 67.03% <ø> (ø)
rust.harpc-types 0.00% <ø> (ø)
rust.harpc-wire-protocol 92.23% <ø> (ø)
rust.hash-codec 72.76% <ø> (ø)
rust.hash-config 81.14% <ø> (ø)
rust.hash-graph-api 19.71% <ø> (ø)
rust.hash-graph-atlas ?
rust.hash-graph-authentication 96.02% <ø> (ø)
rust.hash-graph-authorization 63.14% <ø> (ø)
rust.hash-graph-embeddings 91.88% <ø> (ø)
rust.hash-graph-postgres-store 32.15% <ø> (ø)
rust.hash-graph-store 48.41% <ø> (ø)
rust.hash-graph-temporal-versioning 50.18% <ø> (ø)
rust.hash-graph-types 0.00% <ø> (ø)
rust.hash-graph-validation 84.71% <ø> (ø)
rust.hash-middleware 90.92% <ø> (ø)
rust.hashql-ast 89.63% <ø> (ø)
rust.hashql-compiletest 28.39% <ø> (ø)
rust.hashql-core 79.03% <ø> (+0.11%) ⬆️
rust.hashql-diagnostics 72.51% <ø> (ø)
rust.hashql-eval 79.82% <ø> (ø)
rust.hashql-hir 89.09% <ø> (ø)
rust.hashql-mir 87.92% <ø> (ø)
rust.hashql-syntax-jexpr 94.04% <ø> (ø)
rust.sarif ?
sarif ?
tests.hash-backend-integration ?
unit-tests ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-advanced-security github-advanced-security AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

clippy found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@codspeed-hq

codspeed-hq Bot commented Sep 10, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

⚠️ 6 benchmarks measured no execution time

Nothing ran under measurement, usually because the compiler removed the code under test. These results are not comparable, so they count as unchanged.

Preventing compiler optimizations

✅ 98 untouched benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
⚠️ as_constant < 1 ns < 1 ns N/A
⚠️ constant_equal < 1 ns < 1 ns N/A
⚠️ constant_not_equal < 1 ns < 1 ns N/A
⚠️ access < 1 ns < 1 ns N/A
⚠️ runtime_equal < 1 ns < 1 ns N/A
⚠️ runtime_not_equal < 1 ns < 1 ns N/A

Comparing bm/be-815-atlas-serve-more-than-one-generation-at-a-time (37e33e2) with bm/be-831-hashql-support-type-parameters-in-the-id-macro (d52ac7c)

Open in CodSpeed

@indietyp
indietyp force-pushed the bm/be-815-atlas-serve-more-than-one-generation-at-a-time branch from b72821e to 0038630 Compare September 11, 2026 14:50
@indietyp
indietyp deployed to pull-request September 11, 2026 14:51 — with GitHub Actions Active
@indietyp
indietyp deployed to pull-request September 11, 2026 14:51 — with GitHub Actions Active

// SAFETY: `buffer` holds exactly `source.len()` slots of `u32` with `u32`'s alignment, and
// `as_mut_ptr` points at its first byte.
unsafe { source.clone_to_uninit(buffer.as_mut_ptr().cast::<u8>()) };
fn boxed_clone_empty() {
let source: Box<IdSlice<TestId, Rc<u8>>> = IdVec::new().into_boxed_slice();

let cloned = source.clone();
let source: Box<IdSlice<TestId, Unit>> =
IdVec::from_raw(alloc::vec![Unit, Unit, Unit]).into_boxed_slice();

let cloned = source.clone();
let source: Box<IdSlice<TestId, Aligned>> =
IdVec::from_raw(alloc::vec![Aligned(1), Aligned(2)]).into_boxed_slice();

let cloned = source.clone();
assert_eq!(cloned.len(), 2);
assert_eq!(cloned[TestId::from_usize(0)].0, 1);
assert_eq!(cloned[TestId::from_usize(1)].0, 2);
assert_eq!(cloned.as_raw().as_ptr().addr() % 64, 0);
- Extract error types to dedicated `error.rs` module
- Move lock-related `RemoveError` and locking implementation to
  generation module root
- Create `scratch.rs` and `staging.rs` modules for staging/publishing
  workflow
- Add `ColumnWriter` for document codec serialization
- Consolidate codec imports and remove duplication in edges/locate
  response encoding
- Implement `Serialize` for `ArchivedEntityId` to serialize as entity ID
  string
- Add `Envelope::encode_json` method for JSON document encoding
- Create `CurrentDocument` type for generation responses
- Implement custom serialization for `TranslateDocument` and related
  types
- Add comprehensive tests for JSON encoding with buffer reuse and
  partial values
Document trait's encode method now returns Result instead of panicking
on serialization errors. CurrentDocument and TranslateDocument use
`Report<serde_json::Error>`; other document types use the never type `!`
since they cannot fail.
- Make `LocateRequest` fields public for external use
- Destructure request body in handler signature for clarity
- Clarify `LocateDocumentError` to `Problem` mapping documentation
- Mark `offset_rule` as const-evaluable function
- Serve command now spawns a background maintenance task for generation
  updates
- HTTP server starts before initial generation publication, returning
  503 until ready
- Updated visibility limits to production values (1GB bytes, 8m soft,
  10m hard)
The `PrincipalLimitLayer` requires the request extension written by
`AuthenticationLayer`. Moving `PrincipalLimitLayer` after
`AuthenticationLayer` ensures the extension is available when needed.
Add integration tests validating layer behavior.
- Remove unused `OwnedLegend::heap_bytes` method
- Make error types `CurrentError` and `OpenError` crate-private
- Add `#[must_use]` attributes to `Depth` methods
- Make `Sha256Digest` and related APIs crate-private
- Change `Zoom` to public visibility
- Refactor `NaiveIdentityProvider::new` to use `from_ref` in tests
- Add validation and error handling to `Encoding::open`
- Add `WorldError::TooManyRows` variant with test coverage
- Enable `const` impl for UUID conversion
- Updates tests to use `from_ref` instead of `new` for borrowed bases
- Adds `from_mut` constructor for mutable references
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/deps Relates to third-party dependencies (area) area/infra Relates to version control, CI, CD or IaC (area) area/libs Relates to first-party libraries/crates/packages (area) area/tests New or updated tests type/eng > backend Owned by the @backend team type/eng > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

2 participants