BE-816: atlas: pull fit generations from S3 - #9682
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## bm/be-817-atlas-push-fit-generations-to-s3 #9682 +/- ##
=============================================================================
Coverage ? 12.27%
=============================================================================
Files ? 12
Lines ? 1198
Branches ? 40
=============================================================================
Hits ? 147
Misses ? 1048
Partials ? 3
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
384d83d to
25240e1
Compare
25240e1 to
ffad6be
Compare
| let (admin, connection) = Config::new() | ||
| .host("localhost") | ||
| .port(5432) | ||
| .user("postgres") | ||
| .password("postgres") |
There was a problem hiding this comment.
Semgrep identified a blocking 🔴 issue in your code:
A secret is hard-coded in the application. Secrets stored in source code, such as credentials, identifiers, and other types of sensitive data, can be leaked and used by internal or external malicious actors. It is recommended to rotate the secret and retrieve them from a secure secret vault or Hardware Security Module (HSM), alternatively environment variables can be used if allowed by your company policy.
To resolve this comment:
🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.
💬 Ignore this finding
Reply with Semgrep commands to ignore this finding.
/fp <comment>for false positive/ar <comment>for acceptable risk/other <comment>for all other reasons
Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by hardcoded-password.
You can view more details about this finding in the Semgrep AppSec Platform.
| let (admin, connection) = Config::new() | ||
| .host("localhost") | ||
| .port(5432) | ||
| .user("postgres") | ||
| .password("postgres") |
| &DatabaseConnectionInfo::new( | ||
| DatabaseType::Postgres, | ||
| "postgres".to_owned(), | ||
| "postgres".to_owned(), |
Benchmark results
|
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2002 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 1002 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 3314 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 1527 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 2078 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 1033 | Flame Graph |
policy_resolution_medium
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 102 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 269 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 108 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 133 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 63 | Flame Graph |
policy_resolution_none
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 8 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 3 | Flame Graph |
policy_resolution_small
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 26 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 94 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 27 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 66 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 29 | Flame Graph |
read_scaling_complete
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id;one_depth | 1 entities | Flame Graph | |
| entity_by_id;one_depth | 10 entities | Flame Graph | |
| entity_by_id;one_depth | 25 entities | Flame Graph | |
| entity_by_id;one_depth | 5 entities | Flame Graph | |
| entity_by_id;one_depth | 50 entities | Flame Graph | |
| entity_by_id;two_depth | 1 entities | Flame Graph | |
| entity_by_id;two_depth | 10 entities | Flame Graph | |
| entity_by_id;two_depth | 25 entities | Flame Graph | |
| entity_by_id;two_depth | 5 entities | Flame Graph | |
| entity_by_id;two_depth | 50 entities | Flame Graph | |
| entity_by_id;zero_depth | 1 entities | Flame Graph | |
| entity_by_id;zero_depth | 10 entities | Flame Graph | |
| entity_by_id;zero_depth | 25 entities | Flame Graph | |
| entity_by_id;zero_depth | 5 entities | Flame Graph | |
| entity_by_id;zero_depth | 50 entities | Flame Graph |
read_scaling_linkless
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | 1 entities | Flame Graph | |
| entity_by_id | 10 entities | Flame Graph | |
| entity_by_id | 100 entities | Flame Graph | |
| entity_by_id | 1000 entities | Flame Graph | |
| entity_by_id | 10000 entities | 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
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1
|
Flame Graph |
representative_read_entity_type
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| get_entity_type_by_id | Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba
|
Flame Graph |
representative_read_multiple_entities
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_property | traversal_paths=0 | 0 | |
| entity_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=0 | 0 | |
| link_by_source_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true |
scenarios
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| full_test | query-limited | Flame Graph | |
| full_test | query-unlimited | Flame Graph | |
| linked_queries | query-limited | Flame Graph | |
| linked_queries | query-unlimited | Flame Graph |
🌟 What is the purpose of this PR?
Pull a published generation onto a serving host and switch to it. To serve millions of points, we rely heavily on memory-mapped files. The issue is that you can't mmap object storage, and solutions that allow it, like NFS, have the downside that each page miss is a network fetch and thus has immense latency.
This PR introduces a way to poll a storage backend by specifying
--download, e.g.hash-graph atlas serve --download s3://<bucket>/<prefix>, which polls the remote storage, watches for changes to thecurrentpointer and then proceeds to download the file into local storage.The organising idea is that the downloader and the generation manager share exactly one contract: the local
currentpointer. This decouples the processes and minimises the invasive procedure. Once we're finished, we simply change the localcurrentpointer, which the generation manager then picks up.For review, the properties we rely on are the following:
Download::acquire, thenGenerationRoot::activate_verified). A partial or corrupt download never becomes the served generation. The host keeps serving what it had and tries again on the next poll.Download::synchronize). A promotion landing mid-poll cannot mix two generations' files, and the next poll picks it up whole. An unchanged id costs a local pointer read and a directory stat, so the one-second default cadence is cheap.--download s3://…without--s3fails at startup, before any task starts and without opening the source, so a misconfigured host does not come up serving 503 and logging poll failures.🔍 What does this change?
flowchart LR S["S3 current"] -->|poll| D[Download] D -->|"active/<id>/ into staging, verify"| R[root] R -->|local current| M[GenerationManager] M -->|open · promote · retire| Q[requests]file/generation/download/:Download::synchronizeis one poll (read remotecurrent, reuse or acquire, activate),Download::runis the loop with the shutdown future, andDownloadTaskis what the host retains.GenerationDownloadBackendis the read side of the storage trait, so the tests run against a directory and a fault-injecting fake.file/generation/remote.rs:RemoteRootandRemoteGenerationname thegenerations/{current,previous,repository,active}layout once, andUploadbuilds its paths from the same type.GenerationRoot::activate_verified: verification under the root lock, withActivateErrorgainingOpenandIntegrityfor what it can now find.--downloadand--download-poll-interval(HASH_GRAPH_ATLAS_DOWNLOAD*) onserve,S3Argsflattened into the graph binary's serve arguments, andServeOptions.storage.ServingbecomesServe, whoseinto_partstakes a shutdown factory and returns the router, the maintenance future and an optional download future, andapps/hash-graphregisters both futures on its lifecycle.tests/generation_transfer.rsbehind thetest-utilsfeature: a publisher-to-downloader round trip and a corrupt-artifact case, plus the cleanup that keeps a failed assertion from leaving objects or multipart uploads behind. Each case runs in a fresh bucket on a local MinIO,#[ignore]by default.rust-tracing-practicesskill records the rule the download and serve code follows: log an ordinary error withDisplayand anerror_stack::ReportwithDebug.currentfollows the source. The next poll re-points a hand-edited local pointer, so a rollback moves the remotecurrent.warnand retries at the poll cadence, with no backoff.S3Argssets no request timeouts, so the SDK defaults are what bound a poll against an unreachable endpoint, and shutdown waits on it.--unlink-expired-generationsremoves expired local generations, so a--downloadhost without that flag keeps one directory per generation it has served.❓ How to test this?
yarn compose up -d minio, and publish a generation into a bucket with BE-817'sfit --upload s3://<bucket>/atlas.hash-graph atlas serve --download s3://<bucket>/atlas --s3 --s3-endpoint http://localhost:9000 --s3-force-path-style --s3-region us-east-1 --s3-access-key-id dev-s3-access-key-id --s3-secret-access-key dev-s3-secret-access-key. The read API answers 503 until the first download completes and the manager opens it, then serves.yarn workspace @rust/hash-graph-atlas test:integration --test generation_transfer --run-ignored only.