perf(iceberg): loadTable-METADATA cache — zero-REST first-ask (q031 72s→~6s arc) + F19 residual - #1503
perf(iceberg): loadTable-METADATA cache — zero-REST first-ask (q031 72s→~6s arc) + F19 residual#1503aaj3f wants to merge 5 commits into
Conversation
|
Heads-up from the sibling branch That branch adds §2 fail-closed credential handling: in the REST arm, If this PR's zero-REST first-ask path synthesizes/reconstructs a loadTable response with Related, in your favor: that branch deliberately kept the disk |
…sh-collision guard) The disk catalog cache names entries by a 64-bit DefaultHasher of the key (metadata_location / lt_key), so two distinct keys can collide onto one filename and silently serve another table's payload — a strictly worse failure than the content-addressed entries' wrong-snapshot, and #1503's pointer entry keys by table. Store the FULL key in the Envelope and verify equality on read: a mismatch is a clean miss, so hash quality is irrelevant to correctness. Bump CACHE_FORMAT_VERSION 1->2 (old keyless entries miss cleanly via the existing version check). Forced-collision hermetic test. Covers the metadata/scanfiles/ countstats entries; the #1503 pointer rides the same Envelope + version.
eee6435 to
c38d412
Compare
…isted) + F19 with_graph_ref ride-along Design-first sketch (STOP for lead review, no engine code). Persistent credential-free metadata_location pointer + lazy storage/creds so a warm-disk first-ask issues zero loadTable GET / OAuth / S3 — closing q031's + q016's ~2-3s isolated-first-ask residual on top of the shared fact-scan/materialize floor. AJ hard constraint: vended creds NEVER persisted; fetched in-memory per-process only on an actual S3 byte-fetch. Rides the deferred F19 with_graph_ref parent-memo clone + its hermetic test. Open questions for the lead in section 9.
…(store-disambiguated key) with_graph_ref switches to a different graph's store, so it re-creates const_sid_cache fresh (its key is the const IRI alone, store-implicit). But the R2RML parent-lookup memo key carries graph_source_id + as_of_t, so a lookup can never be served for the wrong source — sharing it is sound, and lets PR-8b's query-scoped memo survive a correlated inner-join rebuilt across a with_graph_ref boundary (SERVICE / multi-source default R2RML), the one path with_active_graph's clone did not cover. Hermetic pr8b::parent_memo_survives_with_graph_ref_rebuild proves the DIM parent is scanned once across 5 with_graph_ref rebuilds. Closes the F19 residual; no corpus query exercises that path, so it ships gate-neutral.
…ointer + lazy storage Closes q031's/q016's isolated-first-ask ~2-3s loadTable residual (on top of the shared fact-scan/materialize floor). Two coupled parts: (a) A persisted, credential-free lt_key->metadata_location pointer in disk_catalog_cache.rs (same Envelope+version discipline; POINTER STRING + snapshot_ms + cached_at_ms ONLY — no creds/token/config, asserted by pointer_persists_no_credential_bytes). TTL FLUREE_ICEBERG_LOADTABLE_PTR_TTL_SECS (300s, 0=off) bounds cross-process snapshot staleness; switch FLUREE_ICEBERG_LOADTABLE_PTR_CACHE. An as_of_t forward-guard (pointer_is_usable) refuses a pointer older than a requested min_snapshot_ms — inert until Iceberg time-travel lands (caller passes None; grep r2rml-as-of-t). (b) LazyS3Storage (new lazy_storage.rs) impls SendIcebergStorage over a tokio OnceCell + deferred builder. When the pointer + metadata (slice-2) + parquet (DiskArtifactCache) all hit disk, no read reaches it, so the builder never runs => zero loadTable GET / OAuth. The eager REST client is built up front (NO network: the OAuth token exchange rides the first catalog op, the GET) so only the GET is deferred, zeroing both gate counters. The provider holds fluree:&'a which can't cross the Parquet tokio::spawn, so the 'static builder captures owned Arc handles and runs resolve_rest_load_and_storage — the SAME session-pin ladder the eager path uses (no duplicate). LazyS3Storage is Clone (Arc-shared cell) because SendParquetReader needs S: Clone; clones share one build. Switch off = eager (byte-identical). Lib + R2RML integration suites green (lazy_storage 3/3 incl. concurrent-force-dedup + no-panic error path; pointer 8/8; it_graph_source_r2rml 53/53). Rides the F19 with_graph_ref fix (ebcab75). Deterministic gate (load_table.n=0 AND oauth_token.n=0 disk-warm) + baseline next.
Review catch: the loadTable-metadata pointer rung consulted the disk pointer BEFORE any session pin and served metadata WITHOUT registering a pin. A query that touches the same table twice (correlated re-loads — the reason the pin machinery exists) whose disk-pointer TTL expires BETWEEN the touches could read two different snapshots: touch 1 serves pointer-loc1 (no pin), touch 2 misses the expired pointer, falls to the eager path, and a fresh GET returns loc2 (table advanced). Silent-wrong-results, and strictly worse than the pre-change eager ladder (which consulted the pin first on every call). Fix: (a) the rung consults session.pinned_metadata_location BEFORE the disk pointer — a prior touch's pin wins unconditionally; (b) on a rung hit it registers a location-only pin (new IcebergCatalogSession::pin_metadata_location + a separate location_pins map) so every later touch resolves the SAME snapshot even after TTL expiry. Kept separate from load_tables so cached_load_table never serves a credential-less entry (which would build ambient-cred storage); the eager path reloads only creds and its existing pin-override converges on the pinned location. Hermetic location_pin_keeps_query_on_one_snapshot_across_pointer_ttl_expiry proves first-writer-wins + the cached_load_table non-poisoning. catalog_session 7/7, it_graph_source_r2rml 53/53. Pin-register is in-memory only — does not perturb the disk-warm 0/0 gate counters.
…ilder symptom doc, F19 memo-key rationale, fmt - lazy_storage.rs: |res| res.is_ok() -> Result::is_ok (workspace-denied redundant_closure_for_method_calls in test code). - r2rml.rs: name send_parquet.rs's tokio::spawn as the site a future '&a narrowing refactor would fail at, so the '\''static owned-Arc capture reads as load-bearing. - operator.rs: mirror the store-disambiguation rationale at R2rmlParentMemoKey so the F19 with_graph_ref memo-share (why graph_source_id can't be dropped) is documented where an editor would remove the component. - cargo fmt (own-delta hunks). C2 pointer hardening (full lt_key stored in the Envelope + verified on read) is delivered by get/put_metadata_location adopting slice-2's (key, suffix) read/write signatures during the restack onto the #1491 CACHE_FORMAT_VERSION=2 chain.
6750286 to
6d9fe55
Compare
…nter scope) q029 closes its burndown row at 2.61s in-protocol (F17 + cache stack); cold isolated first-ask ~57s-class (S3-fetch floor). The F17 3-rep baseline's 5 perf violations (q002/q004/q024/q030/q043) are attributed and are NOT F17: all five are UNION/BIND-free so F17's switch-gated code is inert, and it is NOT cache eviction (fetch_bytes≈0; artifact cache 90MB, 5.7x under the 512MiB cap). The wall is the loadTable catalog GET (~1.5s vs ~0.4s at bless — network-path). Filed as F21: the #1503 loadTable-pointer cache is TTL-window-bounded (300s + prune-on-read), so it spares the GET only within ~5min of a table's last touch (only 1 .pointer.json survived on disk). Mechanism question — TTL-window vs per-table/keying gap — deferred to a design sketch. q043's 6-GET edw:name fan-out is noted as an F20 widening candidate (class-constrained lang-tag star). doc-21 gets the dated TTL scope correction to its "warm first-ask = zero GET" claim.
…g note, SWITCHES inventory, fmt - union.rs: add the coupling cross-reference the review asked for at the budget-met lever — count (1)'s soundness rests entirely on the forward/absorb classification in Operator::set_row_budget's doc, so an author who forwards a budget through an order-SENSITIVE operator breaks this lever, not the forwarding site. Documented at the dangerous end. - SWITCHES.md: the standing switch-inventory ask — one page, one row per virtual perf lever (default / mechanism / PR / OFF-restores), the falsy convention, the loadTable TTL governing-bound note (#1503) + F21 pointer, and the corpus-side timeout semantics (timeout_s is CI headroom, not the bar). - cargo fmt (union.rs own-delta hunks). The FLUREE_R2RML_UNION_BUDGET naming nit is addressed by the SWITCHES.md scope row (it gates BIND forwarding too) rather than a rename. The scan early-stop test-seam suggestion is declined on binary-purity grounds (see review response).
…e-METADATA cache (#1500 x #1503) The restack onto the pointer-rung/lazy-storage chain applies textually clean but is semantically wrong in four ways: #1503 did not replace the two sites this branch hardens, it CLONED them into a deferred copy that none of the guards reached. Fixed by unifying, not by patching copies. - rest_session_storage(): ONE helper holding the §2 fail-closed decision, the #1498 session reuse, and the vended/ambient construction. The eager arm and the deferred LazyS3Storage builder now share it. Previously the builder's copy had no §2 guard at all, so any lazy-forced storage build silently downgraded a vended-configured source to the process's ambient AWS identity — the exact fail-open §2 exists to close, resurrected. - hydrated_auth_provider(): pairs hydrate() with create_provider_arc() so a client-construction site cannot ship un-hydrated. #1503's pointer rung added a second create_provider_arc that §3's hydrate never reached, so a ConfigValue::SecretRef hard-errored via resolve()'s fail-closed arm exactly when the pointer cache was warm — i.e. rotation broke on the fast path. Hydration still runs strictly AFTER rest_client_cache_key in both arms. - Typed errors now round-trip the builder's IcebergError channel (new IcebergError::CatalogCredentialsNotVended + a storage_query_error lift) instead of being flattened by a blanket to_string(), which had destroyed the 403 wire codes (err:storage/AccessDenied, err:catalog/CredentialsNotVended) on the lazy path. - §1 now seeds the disk metadata layer from the inline copy. #1503's rung serves a zero-REST resolve only when metadata_from_caches hits, and a fresh process's in-memory cache is empty — so the pre-restack §1 would have starved the rung for exactly the catalogs that vend inline metadata (Snowflake), reviving the ~1-3s loadTable GET with the regression visible only in the cache gate's load_table.n. Seeding from the free inline copy arms the rung one touch EARLIER than #1503 alone. - §4: CACHE_FORMAT_VERSION stays at #1503's 2 (a payload change); our filename re-key needs no bump and is documented as orthogonal. path()'s key param generalized for the lt_key space; the pointer layer gets an explicit scope verdict (catalog-gated + already per-graph-source). Tests: fail-closed on the shared helper, explicit ambient opt-in still resolves, typed errors survive the builder channel, inline metadata seeds both caches. All were invisible to both suites before: ours predates the rung, #1503's predates SecretRef/fail-closed/typed errors.
|
Consolidating at maintainer request. #1503 (the loadTable-METADATA cache — a credential-free pointer + lazy storage for a zero-REST first-ask) is carried forward in #1507 (the R2RML/Iceberg virtual-dataset performance program), which brings the #1475–#1507 lineage forward as a single reviewable unit. Its verification of record is the program's corpus benchmark, published as C2-bench-wave1.md (native == virtual, 0 hash mismatches). The pre-consolidation branch tip is preserved at tag |
loadTable-METADATA cache — persisted credential-free pointer + lazy storage (+ F19 residual)
Stacked on
perf/r2rml-q031-refprune(#1502). Closes the isolated first-ask ~2–3sloadTableREST/OAuth cost that F20/PR-4d left on q031/q016 (on top of the shared fact-scan/materialize floor). 4 commits, +906/−10.The win (deterministic gate — GREEN, both runs)
Warm-disk / cold-memory, fresh process per query. The COUNTERS are the deterministic gate; walls vary with cache warmth (two runs shown — pre- and post- the pin fix, which perturbed nothing):
When a query's metadata (slice-2), scan files (slice-2), and every Parquet file (DiskArtifactCache) are all disk-resident, it now issues zero loadTable GET / OAuth / S3. OFF is byte-identical to the old path; row parity holds. q031's first-ask is 72s → 5.9s across this arc (F20 + this). Full-corpus baseline: 54/54 oracle 0-mismatch, 44/51 ok ≤3s (corpus-order loads stay amortized, so full-run walls track #1502 — this closes the per-query first-ask the amortized baseline hid).
Mechanism
(a) Persisted credential-free pointer (
disk_catalog_cache.rs): alt_key → metadata_locationentry (same Envelope +CACHE_FORMAT_VERSIONdiscipline). Payload is the location +snapshot_ms+cached_at_msONLY — no creds/token/config. SwitchFLUREE_ICEBERG_LOADTABLE_PTR_CACHE; TTLFLUREE_ICEBERG_LOADTABLE_PTR_TTL_SECS(300s, 0=off).(b)
LazyS3Storage(newlazy_storage.rs): implsSendIcebergStorageover a tokioOnceCell+ a deferred builder. The pointer rung inload_table_contextservesmetadata_location(pointer) + metadata (caches) with no S3, and defers storage; the reader only forces the builder on a real S3 miss. The eager REST client is built up front — construction does no network; the OAuth token exchange rides the first catalog op (the GET), so deferring the GET zeroes both counters (the gate'soauth_token.n=0enforces this forever). The'staticbuilder captures owned Arc handles (the provider holdsfluree: &'a, which can't cross the Parquettokio::spawn) and runsresolve_rest_load_and_storage— the SAME session-pin ladder as the eager path (no duplicate; full slice-1 reuse incl. the cross-query put).LazyS3StorageisClonevia an Arc-shared cell (SendParquetReaderrequiresS: Clone); clones share one build.Security (creds never persisted)
Hermetic
pointer_persists_no_credential_bytesasserts the on-disk pointer has exactly{metadata_location, snapshot_ms, cached_at_ms}and no credential/token/secret/access_key/session/sig bytes. Vended creds are fetched in-memory, per process, only when the lazy handle is forced (a real S3 miss).Snapshot-pin correctness (review fix, commit 6750286)
The pointer rung consults
session.pinned_metadata_locationBEFORE the disk pointer (a prior touch's pin wins), and registers a location-only pin on a hit — so a query that touches the same table twice reads ONE snapshot even if the pointer's TTL expires between touches. Kept in a separatelocation_pinsmap socached_load_tablenever serves a credential-less entry (which would build ambient-cred storage); the eager path reloads only creds and converges on the pinned location. Hermeticlocation_pin_keeps_query_on_one_snapshot_across_pointer_ttl_expiry.TTL staleness semantics
The pointer (unlike content-addressed entries) CAN go stale on a table commit, so it carries a TTL = the max cross-process snapshot staleness for a latest-snapshot read — the same tradeoff class the existing 60s in-memory cross-query cache accepts, extended deliberately, consistent with the disk-cache-is-steady-state ruling. A stale pointer never returns WRONG data for the snapshot it names; it can only lag a newer snapshot within the TTL.
as_of_t forward-guard + wiring obligation
Iceberg snapshot time-travel is deliberately unimplemented on the scan path (
grep r2rml-as-of-t), so the caller passesmin_snapshot_ms=None(latest). The guard (pointer_is_usable) + hermeticpointer_ttl_and_as_of_riderexist; doc-21 §4 + the register carry the obligation: whoever implements time-travel MUST thread the requested snapshot ms intoget_metadata_location.F19 residual (ride-along, commit ebcab75)
with_graph_refnow clones the R2RML parent-memo Arc (its key is store-disambiguated(graph_source_id, parent_tm, cols, as_of_t), unlikeconst_sid_cachewhich stays fresh for the store switch). Closes the F19 residual; hermeticparent_memo_survives_with_graph_ref_rebuild. No corpus query exercises SERVICE/multi-source R2RML, so gate-neutral.Tests
lazy_storage 3/3 (deferred-not-invoked, 16-concurrent-forces→1-build, no-panic error path); disk_catalog_cache 8/8; catalog_session 7/7; full
-p fluree-db-api --lib765/0; it_graph_source_r2rml 53/53. All fluree-db-api test targets compile.CI note: base is
perf/r2rml-q031-refprune, notmain, so repo CI does not fire (stacked-PR convention).