Skip to content

FE-1569: Add durable Brunch storage and telemetry - #9487

Merged
lunelson merged 1 commit into
mainfrom
ln/fe-1569-brunch-agent-deployment
Sep 7, 2026
Merged

FE-1569: Add durable Brunch storage and telemetry#9487
lunelson merged 1 commit into
mainfrom
ln/fe-1569-brunch-agent-deployment

Conversation

@lunelson

@lunelson lunelson commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🌟 What is the purpose of this PR?

Add the runtime behavior needed to take the containerized Brunch service to the application-to-infrastructure deployment boundary: durable Flue Postgres state, content-free operational telemetry, and explicit diagnostics for restricted deployment and recovery.

This PR does not claim that Brunch is deployed. The downstack container PR publishes to ECR and GHCR, but no Brunch ECS service, RDS target, hosted collector, restricted ingress, or deployment owner is confirmed, so Mission 8 remains open at its required handoff boundary.

🔗 Related links

🚫 Blocked by

  • Infrastructure owner confirms and provisions the Brunch ECS, RDS/IAM, collector, restricted ingress, and deployment-owner targets.
  • The remote Mission 8 proof matrix passes against one immutable image digest.

🔍 What does this change?

  • Replaces deployed SQLite with fail-closed @flue/postgres persistence using dedicated fields, verified TLS, RDS IAM tokens per physical connection, and a runtime-password fallback.
  • Adds content-free Flue and Node OpenTelemetry export over OTLP/gRPC with graceful lifecycle flush and operational database-failure spans.
  • Adds RDS IAM and restricted deployment probes plus a disposable TLS Postgres/OTel container smoke.
  • Documents runtime configuration, ingress restrictions, local proof, GHCR publication, and the outstanding infrastructure handoff.
🏗️ Agent notes

The branch authority is libs/@hashintel/brunch-agent/MISSION.md. It records Mission 8 as live but stopped at the explicit application-to-infrastructure handoff boundary.

Locally established:

  • fail-closed production configuration and Flue Postgres migration before listen;
  • IAM-token callback refresh, transaction pinning/rollback, TLS verification, and static-password fallback;
  • content-free OTLP traces, logs, and metrics with graceful shutdown;
  • disposable TLS Postgres and OTel collector integration smoke.

Still open:

  • real task-role/RDS IAM connectivity;
  • restricted streamed Anthropic/tool execution;
  • in-place and cross-host replacement recovery;
  • bounded provider/database/stream failure inspection in the hosted telemetry backend;
  • singleton replacement, rollback, and owner acceptance.

The downstack PR registers ECR and GHCR publication with no ECS target. Infrastructure provisioning and remote proof remain outside this local application artifact.

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:

  • require changes to docs which are made as part of this PR

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

The changes in this PR:

  • affected the execution graph, and the turbo.json files have been updated to reflect this

⚠️ Known issues

  • Brunch is not deployed; the deploy catalog publishes to ECR and GHCR but has no ECS target.
  • The local Docker smoke uses the runtime-password path; the real task-role RDS IAM probe remains unrun.
  • /health proves process liveness only. Database connection and migration gate startup before the server listens.
  • Public identity, authorization, rate/spend controls, retention/deletion policy, backup restoration, and multi-replica ownership remain outside this restricted singleton threshold.

🐾 Next steps

  • Provision and record the ECS, RDS/IAM, secret, collector, ingress, health, timeout, drain, and ownership contract from the deployment handoff.
  • Run the remote proof matrix with one immutable digest and update this PR, Mission 8, and FE-1569 with observed results.

🛡 What tests cover this?

  • 80 Vitest unit tests covering configuration, IAM token refresh, Postgres transactions, health, telemetry lifecycle, artifact wiring, and existing Brunch behavior.
  • TypeScript typecheck, application build, and Oxlint with eight pre-existing warnings and no errors.
  • The previously completed Docker integration smoke covers TLS Postgres migration/startup, fail-closed configuration, OTel export, non-root identity, packaged resources, repository immutability, health, and graceful shutdown.

❓ How to test this?

  1. Run yarn workspace @apps/brunch-agent lint:tsc.
  2. Run yarn workspace @apps/brunch-agent test:unit.
  3. Run yarn workspace @apps/brunch-agent build.
  4. Run turbo run test:docker --filter @apps/brunch-agent with Docker available.
  5. After infrastructure exists, follow apps/brunch-agent/README.md and the Mission 8 deployment handoff for the IAM and remote smoke commands.

📹 Demo

No visual UI change. The proof artifact is the production container and its deployment diagnostics.

@vercel

vercel Bot commented Sep 1, 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 7, 2026 4:52pm UTC
hashdotdesign-tokens Ready Ready Preview Sep 7, 2026 4:52pm UTC
petrinaut Ready Ready Preview Sep 7, 2026 4:52pm UTC
petrinaut-docs Ready Ready Preview Sep 7, 2026 4:52pm UTC

Request Review

@github-actions github-actions Bot added 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) type/eng > frontend Owned by the @frontend team area/tests New or updated tests area/apps labels Sep 1, 2026
Comment thread apps/brunch-agent/src/postgres.ts Fixed
@codspeed-hq

codspeed-hq Bot commented Sep 1, 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 ln/fe-1569-brunch-agent-deployment (16054f8) with main (76497be)

Open in CodSpeed

lunelson commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

@TimDiekmann TimDiekmann left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It would be good if we could isolate the Dockerfile changes into an own PR. The changes to .github/workflows/deploy.yml will make sure that the build is covered in CI and will upload it on merge to main. I will make sure that a ECR is set up where we can upload the image to and extend the definition with that.

Comment thread apps/brunch-agent/package.json
Comment thread apps/brunch-agent/docker/Dockerfile
@lunelson
lunelson changed the base branch from ln/fe-1563-redesign-runbook-workpiece to graphite-base/9487 September 2, 2026 11:12
@lunelson
lunelson force-pushed the ln/fe-1569-brunch-agent-deployment branch from 157730c to a118473 Compare September 2, 2026 11:12
@lunelson
lunelson changed the base branch from graphite-base/9487 to ln/fe-1569-brunch-agent-container September 2, 2026 11:12
@lunelson lunelson changed the title FE-1569: Containerize and safely deploy Brunch on HASH infrastructure FE-1569: Add durable Brunch storage and telemetry Sep 2, 2026
@vercel
vercel Bot temporarily deployed to Preview – petrinaut September 2, 2026 11:13 Inactive

Copilot 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.

🟡 Changes recommended

Moderate issues remain in recovery validation, database timeouts, and Flue telemetry export.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds durable PostgreSQL persistence, RDS IAM support, operational telemetry, and deployment diagnostics for the Brunch service.

Changes:

  • Adds fail-closed PostgreSQL configuration with TLS and IAM/password authentication.
  • Adds OTLP telemetry lifecycle and database-failure reporting.
  • Adds deployment probes, integration tests, and infrastructure handoff documentation.
File summaries
File Review
yarn.lock Locks new database, AWS, and telemetry dependencies.
libs/@hashintel/brunch-agent/MISSION.next.md Updates deployment backlog and planning context.
libs/@hashintel/brunch-agent/MISSION.md Defines the restricted-deployment mission.
libs/@hashintel/brunch-agent/docs/evidence/implementations/mission-8-deployment-handoff.md Records proof status and infrastructure handoff.
apps/brunch-agent/turbo.json Adds telemetry environment and integration-test wiring.
apps/brunch-agent/test/telemetry.test.ts Tests telemetry configuration and disposal.
apps/brunch-agent/test/postgres.test.ts Tests pooling, IAM tokens, transactions, and shutdown.
apps/brunch-agent/test/database-config.test.ts Tests production database validation.
apps/brunch-agent/test/container-smoke.integration.ts Exercises the production container with PostgreSQL and OTel.
apps/brunch-agent/test/build-artifact.test.ts Verifies persistence and telemetry bundle wiring.
apps/brunch-agent/test/architecture/boundaries.integration.ts Registers the telemetry architecture boundary.
apps/brunch-agent/src/telemetry.ts Configures OTLP providers and failure spans. Moderate (1 vote): Flue metrics use the unregistered global meter and are not exported. Moderate (1 vote): Flue also receives no logger, dropping its logs as well as metrics.
apps/brunch-agent/src/telemetry-bootstrap.ts Installs telemetry before application initialization.
apps/brunch-agent/src/rds-iam-probe.ts Adds the two-connection IAM diagnostic.
apps/brunch-agent/src/postgres.ts Implements the Flue PostgreSQL runner. Moderate (1 vote): No connection timeout is configured, allowing database operations and diagnostics to wait indefinitely.
apps/brunch-agent/src/deployment-smoke.ts Adds remote turn and history diagnostics. Moderate (2 votes): Recovery can report success for missing/malformed history, and streamed turns do not require a successful finish.
apps/brunch-agent/src/db.ts Selects PostgreSQL in production and SQLite locally.
apps/brunch-agent/src/database-config.ts Defines fail-closed production database configuration.
apps/brunch-agent/src/app.ts Bootstraps telemetry before route setup.
apps/brunch-agent/README.md Documents runtime and deployment requirements.
apps/brunch-agent/package.json Adds dependencies and operational scripts.
apps/brunch-agent/docs/task-dependencies.json Documents the new task dependencies.
Review details

Suppressed comments (4)

apps/brunch-agent/src/postgres.ts:102

  • A pg pool emits an error event when an idle connection is broken; without a listener, Node treats that event as unhandled and terminates the process. A routine RDS restart or network partition can therefore crash Brunch instead of producing the operational database-failure telemetry added by this PR. Attach a pool error listener and report it through reportDatabaseFailure.
export const createPostgresPool = (
  config: PostgresDatabaseConfig,
  options?: ConnectionOptions,
): Pool => new Pool(createPostgresPoolConfig(config, options));

apps/brunch-agent/src/postgres.ts:200

  • If one pool.connect() resolves and the other rejects, the Promise.all assignment never completes, so clients remains empty and the successful checked-out client is omitted from the finally cleanup. The failed IAM probe then leaks that client and can leave pool.end() waiting indefinitely. Acquire while appending each client (the first remains checked out, so the second is still guaranteed to be a distinct physical connection).
  let clients: PoolClient[] = [];
  try {
    clients = await Promise.all([pool.connect(), pool.connect()]);

apps/brunch-agent/src/postgres.ts:128

  • A transaction that fails while acquiring a pooled connection bypasses the try block, so no database_operation failure span is recorded. This is the exact path taken when Postgres is unavailable during Flue startup/migration, contradicting the operational database-failure telemetry contract. Report and rethrow acquisition failures before entering the transaction body.
      const client = await pool.connect();

apps/brunch-agent/src/postgres.ts:156

  • Both expressions passed to Promise.allSettled are invoked immediately, so telemetry shutdown begins before pool.end() has completed. The unit test only observes synchronous mock side effects and therefore does not enforce the documented “close Postgres, then flush OTel” lifecycle; with a real pool, final database activity can race provider shutdown. Await the Postgres result first, then invoke the telemetry closer while still collecting both failures.
      const results = await Promise.allSettled([pool.end(), afterClose?.()]);
  • Files reviewed: 21/22 changed files
  • Comments generated: 4
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/brunch-agent/src/deployment-smoke.ts Outdated
Comment thread apps/brunch-agent/src/postgres.ts
Comment thread apps/brunch-agent/src/telemetry.ts
Comment thread apps/brunch-agent/src/telemetry.ts Outdated
Comment thread apps/brunch-agent/src/telemetry.ts
Comment thread apps/brunch-agent/src/database-config.ts
Comment thread apps/brunch-agent/src/deployment-smoke.ts
Comment thread apps/brunch-agent/src/postgres.ts
Comment thread apps/brunch-agent/src/postgres.ts Outdated
Comment thread apps/brunch-agent/src/postgres.ts
Comment thread apps/brunch-agent/src/postgres.ts Fixed
@lunelson

lunelson commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

@TimDiekmann according to my agent, CI is currently blocked here by an unrelated pruning issue:

  • the workflow initially prunes out @rust/hash-repo-chores
  • the Brunch integration test subsequently performs a nested Docker build
  • that build's turbo prune evaluates turbo.json and encounters the missing @rust/hash-repo-chores#build reference

Failing job: https://github.com/hashintel/hash/actions/runs/33872870612/job/101023099113

FWIW, agent thinks the integration workflow likely needs to retain @rust/hash-repo-chores, as the unit workflow already does. Rerunning the job alone will not fix it... Thoughts?

Comment thread apps/brunch-agent/package.json Outdated
Comment thread apps/brunch-agent/package.json Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread apps/brunch-agent/test/container-smoke.ts
TimDiekmann
TimDiekmann previously approved these changes Sep 7, 2026

@TimDiekmann TimDiekmann left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Infra ✅

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread apps/brunch-agent/src/telemetry.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread apps/brunch-agent/src/postgres.ts
Comment thread apps/brunch-agent/src/postgres.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread apps/brunch-agent/src/telemetry.ts
Comment thread apps/brunch-agent/src/postgres.ts

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

Comment thread apps/brunch-agent/docs/task-dependencies.json
Comment thread apps/brunch-agent/src/postgres.ts Dismissed
TimDiekmann
TimDiekmann previously approved these changes Sep 7, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b1bd896. Configure here.

Comment thread apps/brunch-agent/src/telemetry.ts
Co-authored-by: Cursor <cursoragent@cursor.com>

Normalize deployment dependency lockfile

Co-authored-by: Cursor <cursoragent@cursor.com>

Publish the Brunch image to GHCR

Co-authored-by: Cursor <cursoragent@cursor.com>
@github-actions

github-actions Bot commented Sep 7, 2026

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 $$27.9 \mathrm{ms} \pm 194 \mathrm{μs}\left({\color{gray}-0.888 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$3.63 \mathrm{ms} \pm 25.0 \mathrm{μs}\left({\color{gray}-0.515 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 1002 $$13.8 \mathrm{ms} \pm 102 \mathrm{μs}\left({\color{gray}-0.661 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 3314 $$45.8 \mathrm{ms} \pm 421 \mathrm{μs}\left({\color{gray}1.61 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$15.5 \mathrm{ms} \pm 164 \mathrm{μs}\left({\color{gray}1.63 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 1527 $$25.4 \mathrm{ms} \pm 192 \mathrm{μs}\left({\color{gray}-0.951 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 2078 $$29.3 \mathrm{ms} \pm 188 \mathrm{μs}\left({\color{gray}-0.914 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$4.00 \mathrm{ms} \pm 31.9 \mathrm{μs}\left({\color{gray}-0.546 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 1033 $$15.1 \mathrm{ms} \pm 105 \mathrm{μs}\left({\color{gray}1.48 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_medium

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 102 $$3.99 \mathrm{ms} \pm 27.2 \mathrm{μs}\left({\color{gray}-0.611 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$3.14 \mathrm{ms} \pm 19.4 \mathrm{μs}\left({\color{gray}-0.190 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 52 $$3.57 \mathrm{ms} \pm 22.4 \mathrm{μs}\left({\color{gray}-0.477 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 269 $$5.54 \mathrm{ms} \pm 41.5 \mathrm{μs}\left({\color{gray}0.697 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$3.74 \mathrm{ms} \pm 26.3 \mathrm{μs}\left({\color{gray}-0.345 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 108 $$4.42 \mathrm{ms} \pm 33.3 \mathrm{μs}\left({\color{gray}1.62 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 133 $$4.78 \mathrm{ms} \pm 42.8 \mathrm{μs}\left({\color{gray}0.747 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$3.63 \mathrm{ms} \pm 23.3 \mathrm{μs}\left({\color{gray}-0.410 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 63 $$4.51 \mathrm{ms} \pm 37.4 \mathrm{μs}\left({\color{gray}4.23 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_none

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 2 $$2.87 \mathrm{ms} \pm 20.2 \mathrm{μs}\left({\color{gray}0.354 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.82 \mathrm{ms} \pm 18.7 \mathrm{μs}\left({\color{gray}0.193 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 2 $$2.95 \mathrm{ms} \pm 17.8 \mathrm{μs}\left({\color{gray}-0.344 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 8 $$3.24 \mathrm{ms} \pm 28.4 \mathrm{μs}\left({\color{gray}0.655 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$3.00 \mathrm{ms} \pm 16.3 \mathrm{μs}\left({\color{gray}0.183 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 3 $$3.34 \mathrm{ms} \pm 24.6 \mathrm{μs}\left({\color{gray}1.08 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_small

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 52 $$3.24 \mathrm{ms} \pm 26.0 \mathrm{μs}\left({\color{gray}-0.802 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.91 \mathrm{ms} \pm 16.6 \mathrm{μs}\left({\color{gray}-1.442 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 26 $$3.14 \mathrm{ms} \pm 23.2 \mathrm{μs}\left({\color{gray}0.460 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 94 $$3.72 \mathrm{ms} \pm 28.9 \mathrm{μs}\left({\color{gray}0.736 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$3.22 \mathrm{ms} \pm 23.0 \mathrm{μs}\left({\color{gray}-0.089 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 27 $$3.50 \mathrm{ms} \pm 25.7 \mathrm{μs}\left({\color{gray}0.102 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 66 $$3.62 \mathrm{ms} \pm 28.3 \mathrm{μs}\left({\color{gray}-1.460 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$3.22 \mathrm{ms} \pm 23.4 \mathrm{μs}\left({\color{gray}0.385 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 29 $$3.55 \mathrm{ms} \pm 31.9 \mathrm{μs}\left({\color{gray}-0.283 \mathrm{\%}}\right) $$ Flame Graph

read_scaling_complete

Function Value Mean Flame graphs
entity_by_id;one_depth 1 entities $$33.7 \mathrm{ms} \pm 267 \mathrm{μs}\left({\color{gray}0.564 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 10 entities $$73.5 \mathrm{ms} \pm 520 \mathrm{μs}\left({\color{gray}-0.883 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 25 entities $$37.5 \mathrm{ms} \pm 227 \mathrm{μs}\left({\color{gray}0.370 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 5 entities $$41.0 \mathrm{ms} \pm 266 \mathrm{μs}\left({\color{gray}-0.372 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 50 entities $$44.6 \mathrm{ms} \pm 228 \mathrm{μs}\left({\color{gray}1.47 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 1 entities $$35.5 \mathrm{ms} \pm 191 \mathrm{μs}\left({\color{gray}0.942 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 10 entities $$424 \mathrm{ms} \pm 1.30 \mathrm{ms}\left({\color{gray}-0.939 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 25 entities $$93.1 \mathrm{ms} \pm 611 \mathrm{μs}\left({\color{gray}-1.137 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 5 entities $$81.2 \mathrm{ms} \pm 539 \mathrm{μs}\left({\color{gray}-1.358 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 50 entities $$312 \mathrm{ms} \pm 1.18 \mathrm{ms}\left({\color{red}9.95 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 1 entities $$11.0 \mathrm{ms} \pm 58.0 \mathrm{μs}\left({\color{gray}-0.384 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 10 entities $$11.3 \mathrm{ms} \pm 72.3 \mathrm{μs}\left({\color{gray}0.203 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 25 entities $$11.3 \mathrm{ms} \pm 64.7 \mathrm{μs}\left({\color{gray}-0.797 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 5 entities $$11.0 \mathrm{ms} \pm 56.8 \mathrm{μs}\left({\color{gray}-1.839 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 50 entities $$11.2 \mathrm{ms} \pm 66.9 \mathrm{μs}\left({\color{gray}-0.375 \mathrm{\%}}\right) $$ Flame Graph

read_scaling_linkless

Function Value Mean Flame graphs
entity_by_id 1 entities $$11.0 \mathrm{ms} \pm 66.0 \mathrm{μs}\left({\color{gray}-0.545 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 10 entities $$11.1 \mathrm{ms} \pm 69.9 \mathrm{μs}\left({\color{gray}-0.048 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 100 entities $$11.2 \mathrm{ms} \pm 48.6 \mathrm{μs}\left({\color{gray}0.524 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 1000 entities $$11.2 \mathrm{ms} \pm 60.0 \mathrm{μs}\left({\color{gray}-0.979 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 10000 entities $$11.5 \mathrm{ms} \pm 84.7 \mathrm{μs}\left({\color{gray}0.198 \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 $$11.5 \mathrm{ms} \pm 79.4 \mathrm{μs}\left({\color{gray}-1.771 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1 $$11.8 \mathrm{ms} \pm 94.8 \mathrm{μs}\left({\color{gray}-2.014 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1 $$11.5 \mathrm{ms} \pm 72.1 \mathrm{μs}\left({\color{gray}-1.204 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1 $$11.5 \mathrm{ms} \pm 78.0 \mathrm{μs}\left({\color{gray}-0.439 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2 $$11.5 \mathrm{ms} \pm 77.1 \mathrm{μs}\left({\color{gray}-0.684 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1 $$11.6 \mathrm{ms} \pm 92.7 \mathrm{μs}\left({\color{gray}0.703 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1 $$11.5 \mathrm{ms} \pm 68.4 \mathrm{μs}\left({\color{gray}-1.530 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1 $$11.5 \mathrm{ms} \pm 77.8 \mathrm{μs}\left({\color{gray}-1.420 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1 $$11.6 \mathrm{ms} \pm 75.7 \mathrm{μs}\left({\color{gray}-1.443 \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 $$8.49 \mathrm{ms} \pm 84.4 \mathrm{μs}\left({\color{gray}-0.669 \mathrm{\%}}\right) $$ Flame Graph

representative_read_multiple_entities

Function Value Mean Flame graphs
entity_by_property traversal_paths=0 0 $$59.0 \mathrm{ms} \pm 570 \mathrm{μs}\left({\color{gray}-0.006 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=255 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true $$113 \mathrm{ms} \pm 769 \mathrm{μs}\left({\color{gray}0.571 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false $$65.2 \mathrm{ms} \pm 484 \mathrm{μs}\left({\color{gray}-0.913 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true $$74.4 \mathrm{ms} \pm 568 \mathrm{μs}\left({\color{gray}0.548 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true $$83.1 \mathrm{ms} \pm 491 \mathrm{μs}\left({\color{gray}-1.165 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true $$90.3 \mathrm{ms} \pm 652 \mathrm{μs}\left({\color{gray}-0.019 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=0 0 $$47.6 \mathrm{ms} \pm 319 \mathrm{μs}\left({\color{gray}0.625 \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 $$76.0 \mathrm{ms} \pm 655 \mathrm{μs}\left({\color{gray}-0.570 \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 $$54.3 \mathrm{ms} \pm 391 \mathrm{μs}\left({\color{gray}0.682 \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 $$62.7 \mathrm{ms} \pm 357 \mathrm{μs}\left({\color{gray}-0.110 \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 $$65.1 \mathrm{ms} \pm 402 \mathrm{μs}\left({\color{gray}0.802 \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 $$64.7 \mathrm{ms} \pm 435 \mathrm{μs}\left({\color{gray}-0.541 \mathrm{\%}}\right) $$

scenarios

Function Value Mean Flame graphs
full_test query-limited $$118 \mathrm{ms} \pm 625 \mathrm{μs}\left({\color{gray}4.14 \mathrm{\%}}\right) $$ Flame Graph
full_test query-unlimited $$129 \mathrm{ms} \pm 776 \mathrm{μs}\left({\color{gray}2.89 \mathrm{\%}}\right) $$ Flame Graph
linked_queries query-limited $$24.7 \mathrm{ms} \pm 182 \mathrm{μs}\left({\color{gray}-0.392 \mathrm{\%}}\right) $$ Flame Graph
linked_queries query-unlimited $$510 \mathrm{ms} \pm 1.48 \mathrm{ms}\left({\color{gray}-0.567 \mathrm{\%}}\right) $$ Flame Graph

lunelson commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Merge activity

  • Sep 7, 5:54 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Sep 7, 5:54 PM UTC: Graphite couldn't add this PR to the GitHub merge queue because it failed for an unknown reason.

host: "BRUNCH_POSTGRES_HOST",
password: "BRUNCH_POSTGRES_PASSWORD",
port: "BRUNCH_POSTGRES_PORT",
tlsCaPath: "BRUNCH_POSTGRES_TLS_CA_PATH",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should not be a required environment. It should be possible to connect to Postgres without https. The other way would be to provide the CA certificate in the Dockerfile instead, but making it not required is the easiest way for now.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is largely a copy of the existing setup from @local/hash-backend-utils. The least thing we should do is cross-link through a comment and add a TODO with a ticket attached, so we unify these two.

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.

Addressed in #9572: Brunch now delegates provider and instrumentation registration to @local/hash-backend-utils/opentelemetry instead of maintaining the copied setup.

Comment on lines +54 to +60
const absent = (environment: Environment, name: string): void => {
if (environment[name] !== undefined) {
throw new Error(
`Production database configuration does not accept ${name}.`,
);
}
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Error on an env-var being set is a weird behavior, it should at most warn.

Comment on lines +84 to +86
if (environment.NODE_ENV !== "production") {
return { kind: "sqlite" };
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why we cannot use Postgres in non-production builds?

Comment on lines +183 to +188
if (
environment.NODE_ENV === "production" &&
(endpoint === undefined || endpoint.trim().length === 0)
) {
throw new Error("Production telemetry requires HASH_OTLP_ENDPOINT.");
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should not error but warn instead.

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.

Addressed in #9572: a missing or failed telemetry exporter setup no longer aborts Brunch startup; it warns and continues without exporters.

}
},
close: async () => {
const results = await Promise.allSettled([pool.end(), afterClose?.()]);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

on pool.end(), the telemetry shutdown runs in parallel, but the corresponding test suggests there is a strict order

Comment on lines +79 to +90
const common: PoolConfig = {
application_name: "brunch-agent",
connectionTimeoutMillis: POSTGRES_CONNECTION_TIMEOUT_MS,
database: config.database,
host: config.host,
port: config.port,
ssl: {
ca: readTlsCa(config.tlsCaPath),
rejectUnauthorized: true,
},
user: config.user,
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We should set a query/statement timeout

Comment on lines +19 to +28
try {
config = loadDatabaseConfig();
} catch (error) {
try {
await recordOperationalFailure("database_configuration", error);
} catch {
// The database configuration error remains the authoritative startup cause.
}
throw error;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't reading the CA file and pool-functions be wrapped as well?

});
};

const registerHashOpenTelemetry = ({

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It appears that this does not include peer.service (Which is important for service graphs), and never calls the global meter provider. I tend to say we should use the one in hash-backend-utils instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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 > frontend Owned by the @frontend team

Development

Successfully merging this pull request may close these issues.

4 participants