diff --git a/.env.example b/.env.example index f47b694..3ae48a3 100644 --- a/.env.example +++ b/.env.example @@ -196,6 +196,8 @@ PRISM_PUBLIC_CACHE_TTL_SECONDS=5 PRISM_PUBLIC_CACHE_STALE_WHILE_REVALIDATE_SECONDS=30 PRISM_PUBLIC_CONFIG_CACHE_TTL_SECONDS=300 PRISM_PUBLIC_CONFIG_CACHE_STALE_WHILE_REVALIDATE_SECONDS=3600 +PRISM_PUBLIC_SETTLEMENT_CACHE_TTL_SECONDS= +PRISM_PUBLIC_SETTLEMENT_CACHE_STALE_WHILE_REVALIDATE_SECONDS= PRISM_PUBLIC_ARTIFACT_CACHE_TTL_SECONDS=86400 PRISM_PUBLIC_ARTIFACT_CACHE_STALE_WHILE_REVALIDATE_SECONDS=86400 PRISM_PUBLIC_CACHE_MAX_ENTRIES=512 diff --git a/compose.yaml b/compose.yaml index 0dd31a2..8dd2baa 100644 --- a/compose.yaml +++ b/compose.yaml @@ -523,6 +523,8 @@ services: PRISM_PUBLIC_CACHE_STALE_WHILE_REVALIDATE_SECONDS: ${PRISM_PUBLIC_CACHE_STALE_WHILE_REVALIDATE_SECONDS:-30} PRISM_PUBLIC_CONFIG_CACHE_TTL_SECONDS: ${PRISM_PUBLIC_CONFIG_CACHE_TTL_SECONDS:-300} PRISM_PUBLIC_CONFIG_CACHE_STALE_WHILE_REVALIDATE_SECONDS: ${PRISM_PUBLIC_CONFIG_CACHE_STALE_WHILE_REVALIDATE_SECONDS:-3600} + PRISM_PUBLIC_SETTLEMENT_CACHE_TTL_SECONDS: ${PRISM_PUBLIC_SETTLEMENT_CACHE_TTL_SECONDS:-} + PRISM_PUBLIC_SETTLEMENT_CACHE_STALE_WHILE_REVALIDATE_SECONDS: ${PRISM_PUBLIC_SETTLEMENT_CACHE_STALE_WHILE_REVALIDATE_SECONDS:-} PRISM_PUBLIC_ARTIFACT_CACHE_TTL_SECONDS: ${PRISM_PUBLIC_ARTIFACT_CACHE_TTL_SECONDS:-86400} PRISM_PUBLIC_ARTIFACT_CACHE_STALE_WHILE_REVALIDATE_SECONDS: ${PRISM_PUBLIC_ARTIFACT_CACHE_STALE_WHILE_REVALIDATE_SECONDS:-86400} PRISM_PUBLIC_CACHE_MAX_ENTRIES: ${PRISM_PUBLIC_CACHE_MAX_ENTRIES:-512} diff --git a/docs/public-dashboard-api/README.md b/docs/public-dashboard-api/README.md index dbb2cc8..bde80d3 100644 --- a/docs/public-dashboard-api/README.md +++ b/docs/public-dashboard-api/README.md @@ -39,8 +39,14 @@ emits conservative browser caching (`Cache-Control: public, max-age=0, must-revalidate`) plus shared-cache headers for CDNs such as Vercel. Dynamic dashboard read models default to a 5-second shared-cache TTL with 30 seconds of `stale-while-revalidate`. `GET /public/v1/mining-configuration` defaults to 300 -seconds, and content-addressed artifact routes default to 86400 seconds with an -immutable shared-cache hint. +seconds. Settlement-artifact responses also default to 5 seconds because they +include live fanout status and broadcast attempts. Content-addressed artifact +routes default to 86400 seconds with an immutable shared-cache hint. + +Settlement-specific cache controls are blank by default and inherit +`PRISM_PUBLIC_CACHE_TTL_SECONDS` and +`PRISM_PUBLIC_CACHE_STALE_WHILE_REVALIDATE_SECONDS`. Set them only when an +explicit route-specific override is required. Operators can tune the defaults with: @@ -49,6 +55,8 @@ Operators can tune the defaults with: - `PRISM_PUBLIC_CACHE_STALE_WHILE_REVALIDATE_SECONDS` - `PRISM_PUBLIC_CONFIG_CACHE_TTL_SECONDS` - `PRISM_PUBLIC_CONFIG_CACHE_STALE_WHILE_REVALIDATE_SECONDS` +- `PRISM_PUBLIC_SETTLEMENT_CACHE_TTL_SECONDS` +- `PRISM_PUBLIC_SETTLEMENT_CACHE_STALE_WHILE_REVALIDATE_SECONDS` - `PRISM_PUBLIC_ARTIFACT_CACHE_TTL_SECONDS` - `PRISM_PUBLIC_ARTIFACT_CACHE_STALE_WHILE_REVALIDATE_SECONDS` - `PRISM_PUBLIC_CACHE_MAX_ENTRIES` diff --git a/lab/prism/public_api.py b/lab/prism/public_api.py index 12be27d..d345639 100644 --- a/lab/prism/public_api.py +++ b/lab/prism/public_api.py @@ -188,6 +188,11 @@ def public_cache_key(path: str, query: dict[str, list[str]]) -> tuple[str, tuple def public_cache_policy(path: str) -> PublicCachePolicy: if not env_bool("PRISM_PUBLIC_CACHE_ENABLED", default=True): return PublicCachePolicy(ttl_seconds=0, stale_while_revalidate_seconds=0) + dynamic_ttl_seconds = env_nonnegative_int("PRISM_PUBLIC_CACHE_TTL_SECONDS", 5) + dynamic_stale_while_revalidate_seconds = env_nonnegative_int( + "PRISM_PUBLIC_CACHE_STALE_WHILE_REVALIDATE_SECONDS", + 30, + ) if path == "/public/v1/mining-configuration": return PublicCachePolicy( ttl_seconds=env_nonnegative_int("PRISM_PUBLIC_CONFIG_CACHE_TTL_SECONDS", 300), @@ -196,6 +201,19 @@ def public_cache_policy(path: str) -> PublicCachePolicy: 3600, ), ) + if path.startswith("/public/v1/blocks/") and path.endswith("/settlement-artifacts"): + block_hash = path[len("/public/v1/blocks/"):-len("/settlement-artifacts")].strip() + if _is_hex64(block_hash): + return PublicCachePolicy( + ttl_seconds=env_nonnegative_int( + "PRISM_PUBLIC_SETTLEMENT_CACHE_TTL_SECONDS", + dynamic_ttl_seconds, + ), + stale_while_revalidate_seconds=env_nonnegative_int( + "PRISM_PUBLIC_SETTLEMENT_CACHE_STALE_WHILE_REVALIDATE_SECONDS", + dynamic_stale_while_revalidate_seconds, + ), + ) if path.startswith("/public/v1/artifacts/"): return PublicCachePolicy( ttl_seconds=env_nonnegative_int("PRISM_PUBLIC_ARTIFACT_CACHE_TTL_SECONDS", 86400), @@ -206,11 +224,8 @@ def public_cache_policy(path: str) -> PublicCachePolicy: immutable=True, ) return PublicCachePolicy( - ttl_seconds=env_nonnegative_int("PRISM_PUBLIC_CACHE_TTL_SECONDS", 5), - stale_while_revalidate_seconds=env_nonnegative_int( - "PRISM_PUBLIC_CACHE_STALE_WHILE_REVALIDATE_SECONDS", - 30, - ), + ttl_seconds=dynamic_ttl_seconds, + stale_while_revalidate_seconds=dynamic_stale_while_revalidate_seconds, ) diff --git a/tests/test_prism_compose_profile.py b/tests/test_prism_compose_profile.py index 022fffb..f9e3d0b 100644 --- a/tests/test_prism_compose_profile.py +++ b/tests/test_prism_compose_profile.py @@ -172,6 +172,8 @@ def test_prism_coordinator_gets_required_environment(self) -> None: self.assertEqual(env["PRISM_PUBLIC_CACHE_STALE_WHILE_REVALIDATE_SECONDS"], "30") self.assertEqual(env["PRISM_PUBLIC_CONFIG_CACHE_TTL_SECONDS"], "300") self.assertEqual(env["PRISM_PUBLIC_CONFIG_CACHE_STALE_WHILE_REVALIDATE_SECONDS"], "3600") + self.assertEqual(env["PRISM_PUBLIC_SETTLEMENT_CACHE_TTL_SECONDS"], "") + self.assertEqual(env["PRISM_PUBLIC_SETTLEMENT_CACHE_STALE_WHILE_REVALIDATE_SECONDS"], "") self.assertEqual(env["PRISM_PUBLIC_ARTIFACT_CACHE_TTL_SECONDS"], "86400") self.assertEqual(env["PRISM_PUBLIC_ARTIFACT_CACHE_STALE_WHILE_REVALIDATE_SECONDS"], "86400") self.assertEqual(env["PRISM_PUBLIC_CACHE_MAX_ENTRIES"], "512") diff --git a/tests/test_prism_public_dashboard_api.py b/tests/test_prism_public_dashboard_api.py index 10e612e..f950c5d 100644 --- a/tests/test_prism_public_dashboard_api.py +++ b/tests/test_prism_public_dashboard_api.py @@ -844,6 +844,8 @@ def test_public_api_cache_headers_use_route_specific_ttls(self) -> None: os.environ, { "PRISM_PUBLIC_CONFIG_CACHE_TTL_SECONDS": "600", + "PRISM_PUBLIC_SETTLEMENT_CACHE_TTL_SECONDS": "15", + "PRISM_PUBLIC_SETTLEMENT_CACHE_STALE_WHILE_REVALIDATE_SECONDS": "10", "PRISM_PUBLIC_ARTIFACT_CACHE_TTL_SECONDS": "7200", }, clear=True, @@ -854,6 +856,12 @@ def test_public_api_cache_headers_use_route_specific_ttls(self) -> None: ) as config_response: json.loads(config_response.read()) config_cache = config_response.headers.get("CDN-Cache-Control") + with urllib.request.urlopen( + f"http://127.0.0.1:{server.server_port}/public/v1/blocks/{ledger.block_hash}/settlement-artifacts", + timeout=5, + ) as settlement_response: + json.loads(settlement_response.read()) + settlement_cache = settlement_response.headers.get("CDN-Cache-Control") with urllib.request.urlopen( f"http://127.0.0.1:{server.server_port}/public/v1/artifacts/{ledger.audit_bundle_sha256}", timeout=5, @@ -866,8 +874,34 @@ def test_public_api_cache_headers_use_route_specific_ttls(self) -> None: thread.join(timeout=5) self.assertEqual(config_cache, "public, max-age=600, stale-while-revalidate=3600") + self.assertEqual(settlement_cache, "public, max-age=15, stale-while-revalidate=10") self.assertEqual(artifact_cache, "public, max-age=7200, stale-while-revalidate=86400, immutable") + def test_settlement_artifact_cache_default_stays_short_for_live_status(self) -> None: + path = f"/public/v1/blocks/{FakePublicLedger.block_hash}/settlement-artifacts" + + with patch.dict(os.environ, {}, clear=True): + policy = public_api.public_cache_policy(path) + + self.assertEqual(policy.ttl_seconds, 5) + self.assertEqual(policy.stale_while_revalidate_seconds, 30) + + def test_settlement_artifact_cache_inherits_global_overrides(self) -> None: + path = f"/public/v1/blocks/{FakePublicLedger.block_hash}/settlement-artifacts" + + with patch.dict( + os.environ, + { + "PRISM_PUBLIC_CACHE_TTL_SECONDS": "0", + "PRISM_PUBLIC_CACHE_STALE_WHILE_REVALIDATE_SECONDS": "7", + }, + clear=True, + ): + policy = public_api.public_cache_policy(path) + + self.assertEqual(policy.ttl_seconds, 0) + self.assertEqual(policy.stale_while_revalidate_seconds, 7) + def test_public_api_errors_use_public_error_schema(self) -> None: with self.assertRaises(urllib.error.HTTPError) as raised: self.get_json("/public/v1/leaderboard?window=24h")