tidaldb/tidal/tests/pg1_instrumented_metrics.rs
jordan cdbe9cb453 Merge remote-tracking branch 'origin/main' (m11/m12 cluster) into m9/m10
Reconciles two independently-developed lines from base 006d3d0:
  ours   — M9/M10 community layers, retroactive purge + re-materialization,
           signal revocation, agent capability boundaries, P1 feedback loop,
           reason labels, instrumented metrics
  theirs — M11/M12 cluster mode (tidal-net gRPC transport, tidal-server
           cluster/scatter-gather, tidal-stress), multi-vector preference,
           ANN candidate-gen, warm-tier day buckets, keyed signal snapshots

Notable semantic resolutions:

* storage::keys::Tag — both sides allocated 0x0E..0x11 for different
  records. Kept theirs' 0x0E..0x1A (shipped on-disk format) and renumbered
  ours to 0x1B..0x1E (CommunityMembership/Revocation/PurgeManifest/
  CommunityLeave); Tag::ALL grown to 30 so the contiguity drift guard holds.

* ranking executor — took theirs' rewrite (SignalReadPlan pre-pass, keyed
  SignalKey snapshots, Result-returning reads, finalize()) and re-applied
  ours' M10 read suppression at the chokepoints it introduced:
  single_signal_score, score_hot/trending/controversial,
  CreatorEngagementRate, and the Stage-4 boost loop.

* signals::warm — theirs' day-bucket/read-time-rotation rewrite, with ours'
  subtract_bucket and Clone extended to the new day tier; ours' test split
  kept (warm/tests.rs, warm/proptests.rs) carrying theirs' updated bodies.

* db::signals — kept ours' contribution-logging try_cohort_attribution in
  signal_dispatch.rs and theirs' event-time try_update_preference_vector;
  dropped the superseded duplicates.

* db::mod / from_parts — theirs' constructors, with ours' purge/
  re-materialization/revocation/community/skip-counter fields and restart
  rebuilds; from_parts kept in its own file per the 600-line guideline.

* schema::validation::builders — ours' module split with theirs' expanded
  tests; policy validation runs both sides' checks (read-signal lists +
  profile overrides, then the zero-duration limit guard).

* feedback Unhide no longer writes a -1.0 "hide" signal: theirs' engine
  rejects negative weights (spec §8). Reverses index state only, matching
  every other undo action.

* SessionState::new is now the single construction path (gains
  overrides_rejected/default_profile); AuditEntry gains kind on the
  deserialize path, inferred from the accepted flag as before.

* Removed tidal/src/replication/tcp_transport.rs and its test: never
  declared in replication/mod.rs on either branch, so it had never
  compiled and nothing referenced it. Superseded by tidal-net's
  GrpcTransport.

Verified: cargo clippy -p tidaldb (lib) clean; --all-targets compiles for
tidaldb/tidal-net/tidal-server/tidal-stress; 2094/2094 lib tests and the
integration suite pass except m8p3_reconcile_production's two CRDT-count
assertions, which fail identically on MERGE_HEAD (pre-existing).
tidalctl cannot build locally: its aws-sdk deps need rustc 1.91.1, local
toolchain is 1.91.0.
2026-08-03 02:16:04 -06:00

231 lines
6.9 KiB
Rust

#![allow(clippy::unwrap_used)]
//! PG1 Instrumented Metrics Pipeline — Integration Tests
//!
//! End-to-end tests verifying per-signal-type counters, latency percentiles,
//! personalization staleness, feedback-loop latency, and /diagnostics endpoint.
//!
//! Requires `--features metrics,test-utils`.
use std::collections::HashMap;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::time::Duration;
use tidaldb::TidalDb;
use tidaldb::schema::{DecaySpec, EntityId, EntityKind, SchemaBuilder, Timestamp, Window};
/// Build a schema with view and like signals.
fn test_schema() -> tidaldb::schema::Schema {
let mut builder = SchemaBuilder::new();
let _ = builder
.signal(
"view",
EntityKind::Item,
DecaySpec::Exponential {
half_life: Duration::from_secs(7 * 24 * 3600),
},
)
.windows(&[Window::OneHour, Window::TwentyFourHours])
.velocity(false)
.add();
let _ = builder
.signal(
"like",
EntityKind::Item,
DecaySpec::Exponential {
half_life: Duration::from_secs(7 * 24 * 3600),
},
)
.windows(&[Window::OneHour])
.velocity(false)
.add();
builder.build().unwrap()
}
/// Make an HTTP GET request to the given address and path.
fn http_get(addr: std::net::SocketAddr, path: &str) -> (u16, String) {
let mut stream = TcpStream::connect(addr).expect("connect");
write!(
stream,
"GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
)
.unwrap();
stream.flush().unwrap();
let mut response = String::new();
stream.read_to_string(&mut response).unwrap();
let status: u16 = response
.lines()
.next()
.and_then(|l| l.split_whitespace().nth(1))
.and_then(|s| s.parse().ok())
.unwrap_or(0);
let body = response
.split_once("\r\n\r\n")
.map_or_else(String::new, |(_, b)| b.to_string());
(status, body)
}
fn open_db_with_metrics() -> (std::net::SocketAddr, TidalDb) {
let db = TidalDb::builder()
.ephemeral()
.with_schema(test_schema())
.enable_metrics("127.0.0.1:0")
.open()
.expect("open should succeed");
let addr = db
.metrics_addr()
.expect("metrics_addr should be Some when metrics enabled");
(addr, db)
}
// ── Tests ──────────────────────────────────────────────────────────────────
#[test]
fn prometheus_contains_per_type_counters() {
let (addr, db) = open_db_with_metrics();
// Write some signals of different types.
for i in 1..=10 {
db.signal("view", EntityId::new(i), 1.0, Timestamp::now())
.unwrap();
}
for i in 1..=5 {
db.signal("like", EntityId::new(i), 1.0, Timestamp::now())
.unwrap();
}
let (status, body) = http_get(addr, "/metrics");
assert_eq!(status, 200);
// Verify per-type counters.
assert!(
body.contains("tidaldb_signal_writes_by_type{signal_type=\"view\"}"),
"should contain view per-type counter"
);
assert!(
body.contains("tidaldb_signal_writes_by_type{signal_type=\"like\"}"),
"should contain like per-type counter"
);
db.close().unwrap();
}
#[test]
fn prometheus_contains_percentile_gauges_after_signals() {
let (addr, db) = open_db_with_metrics();
// Write enough signals to populate the latency histogram.
for i in 1..=20 {
db.signal("view", EntityId::new(i), 1.0, Timestamp::now())
.unwrap();
}
let (status, body) = http_get(addr, "/metrics");
assert_eq!(status, 200);
// After writing signals, the signal_write_latency histogram should have data,
// so percentile gauges should appear.
assert!(
body.contains("tidaldb_signal_write_latency_us_p50"),
"should contain signal write p50 gauge"
);
assert!(
body.contains("tidaldb_signal_write_latency_us_p95"),
"should contain signal write p95 gauge"
);
assert!(
body.contains("tidaldb_signal_write_latency_us_p99"),
"should contain signal write p99 gauge"
);
db.close().unwrap();
}
#[test]
fn prometheus_contains_staleness_histogram_after_user_query() {
let (addr, db) = open_db_with_metrics();
// Write item metadata so retrieve has something to find.
let mut meta = HashMap::new();
meta.insert("title".to_string(), "Test Item".to_string());
meta.insert("category".to_string(), "tech".to_string());
db.write_item_with_metadata(EntityId::new(1), &meta)
.unwrap();
// Write a signal with user context.
db.signal_with_context(
"view",
EntityId::new(1),
1.0,
Timestamp::now(),
Some(42),
None,
)
.unwrap();
// Run a retrieve query for that user.
let query = tidaldb::query::retrieve::Retrieve::builder()
.for_user(42)
.build()
.unwrap();
let _ = db.retrieve(&query);
let (status, body) = http_get(addr, "/metrics");
assert_eq!(status, 200);
// Staleness histogram should be present (even if empty -- the histogram lines always render).
assert!(
body.contains("tidaldb_personalization_staleness_us"),
"should contain staleness histogram"
);
db.close().unwrap();
}
#[test]
fn diagnostics_endpoint_returns_valid_json() {
let (addr, db) = open_db_with_metrics();
// Write some signals to populate metrics.
for i in 1..=5 {
db.signal("view", EntityId::new(i), 1.0, Timestamp::now())
.unwrap();
}
let (status, body) = http_get(addr, "/diagnostics");
assert_eq!(status, 200, "diagnostics should return 200");
// Verify it's valid JSON by checking structure.
assert!(body.starts_with('{'), "should start with {{");
assert!(body.ends_with('}'), "should end with }}");
assert!(
body.contains("\"signal_writes_by_type\""),
"should contain signal_writes_by_type key"
);
assert!(
body.contains("\"signal_write_latency_us\""),
"should contain signal_write_latency_us key"
);
assert!(
body.contains("\"retrieve_latency_us\""),
"should contain retrieve_latency_us key"
);
assert!(
body.contains("\"search_latency_us\""),
"should contain search_latency_us key"
);
assert!(
body.contains("\"personalization_staleness_us\""),
"should contain personalization_staleness_us key"
);
assert!(
body.contains("\"feedback_loop_latency_us\""),
"should contain feedback_loop_latency_us key"
);
// Verify view count is present in the per-type breakdown.
assert!(body.contains("\"view\""), "should contain view signal type");
db.close().unwrap();
}