Repository navigation
feat(read-state): server-owned read state for every client (buzz_v1) #8639
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -776,7 +776,17 @@ mod postgres_tests { | |
| let mut migrations: Vec<_> = MIGRATOR.iter().collect(); | ||
| migrations.sort_by_key(|migration| migration.version); | ||
|
|
||
| assert_eq!(migrations.len(), 73); | ||
| assert_eq!(migrations.len(), 75); | ||
| assert_eq!(migrations[74].version, 75); | ||
| assert!(migrations[74] | ||
| .sql | ||
| .as_str() | ||
| .contains("ADD COLUMN revision BIGINT NOT NULL DEFAULT 0")); | ||
| assert_eq!(migrations[73].version, 74); | ||
| assert!(migrations[73] | ||
| .sql | ||
| .as_str() | ||
| .contains("idx_events_read_timeline")); | ||
| assert_eq!(migrations[72].version, 73); | ||
| assert_eq!(migrations[71].version, 72); | ||
| assert_eq!(migrations[70].version, 71); | ||
|
|
@@ -2425,16 +2435,41 @@ mod postgres_tests { | |
| .sql | ||
| .as_str(); | ||
| assert!(through_message.contains("DROP COLUMN threads_through_timestamp")); | ||
| // 0074 adds the sticky thread unfollow and follow liveness. | ||
| let unfollowed_column = "unfollowed boolean not null default false \ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Optional ( |
||
| check (not unfollowed or root_id <> ''::bytea), \ | ||
| explicit boolean not null default false, \ | ||
| active_at timestamptz not null default now(), "; | ||
| let access_paths = MIGRATOR | ||
| .iter() | ||
| .find(|m| m.version == 74) | ||
| .expect("read-state access path migration") | ||
| .sql | ||
| .as_str(); | ||
| assert!(access_paths.contains( | ||
| "$d$BOOLEAN NOT NULL DEFAULT false CHECK (NOT unfollowed OR root_id <> ''::bytea)$d$" | ||
| )); | ||
| assert!(access_paths | ||
| .contains("('personal_read_frontiers', 'explicit', 'BOOLEAN NOT NULL DEFAULT false')")); | ||
| assert!(access_paths.contains( | ||
| "('personal_read_frontiers', 'active_at', 'TIMESTAMPTZ NOT NULL DEFAULT now()')" | ||
| )); | ||
| for (table, definition) in personal.tables { | ||
| let in_schema = schema.tables.get(&table).map(|schema_definition| { | ||
| if table == "personal_read_accounts" { | ||
| schema_definition.replacen(started_at_column, "", 1) | ||
| schema_definition | ||
| .replacen(started_at_column, "", 1) | ||
| .replacen("revision bigint not null default 0, ", "", 1) | ||
| } else { | ||
| schema_definition.clone() | ||
| } | ||
| }); | ||
| let definition = if table == "personal_read_frontiers" { | ||
| definition.replacen(threads_through_column, through_message_column, 1) | ||
| definition.replacen( | ||
| threads_through_column, | ||
| &format!("{through_message_column}{unfollowed_column}"), | ||
| 1, | ||
| ) | ||
| } else { | ||
| definition | ||
| }; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -456,6 +456,10 @@ pub async fn insert_event_in_transaction( | |
| ) -> Result<(StoredEvent, bool)> { | ||
| let result = insert_event_on(tx, event, channel_id).await?; | ||
| if result.1 { | ||
| if let Some((channel, place)) = crate::personal_read::place(event, channel_id, None) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤖 Optional, so it's a deliberate decision: |
||
| crate::personal_read::record_message(tx, event, channel, place, result.0.received_at) | ||
| .await?; | ||
| } | ||
| crate::operator_listener::enqueue_mentions_in_transaction(tx, event).await?; | ||
| } | ||
| Ok(result) | ||
|
|
@@ -493,8 +497,8 @@ async fn insert_event_on( | |
| .await?; | ||
| let result = sqlx::query( | ||
| r#" | ||
| INSERT INTO events (community_id, id, pubkey, created_at, kind, tags, content, sig, received_at, channel_id, d_tag, not_before) | ||
| VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) | ||
| INSERT INTO events (community_id, id, pubkey, created_at, kind, tags, content, sig, received_at, channel_id, d_tag, not_before, read_timeline) | ||
| VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) | ||
| ON CONFLICT DO NOTHING | ||
| "#, | ||
| ) | ||
|
|
@@ -510,6 +514,7 @@ async fn insert_event_on( | |
| .bind(channel_id) | ||
| .bind(d_tag.as_deref()) | ||
| .bind(not_before) | ||
| .bind(crate::personal_read::place(event, channel_id, None).is_some_and(|(_, p)| p.on_timeline())) | ||
| .execute(tx.conn()) | ||
| .await?; | ||
|
|
||
|
|
@@ -1876,14 +1881,25 @@ pub(crate) async fn insert_event_with_thread_metadata_tx( | |
| let received_at = Utc::now(); | ||
| let d_tag = extract_d_tag(event); | ||
| let not_before = extract_not_before(event); | ||
| let place = match &thread_meta { | ||
| Some(meta) => crate::personal_read::place( | ||
| event, | ||
| Some(meta.channel_id), | ||
| meta.parent_event_id.map(|parent| match meta.root_event_id { | ||
| Some(root) => (root, meta.root_event_created_at, meta.broadcast), | ||
| None => (parent, meta.parent_event_created_at, meta.broadcast), | ||
| }), | ||
| ), | ||
| None => crate::personal_read::place(event, channel_id, None), | ||
| }; | ||
|
|
||
| crate::channel_members::guard_channel_roster_event(tx, kind_i32, channel_id, &tags_json) | ||
| .await?; | ||
|
|
||
| let result = sqlx::query( | ||
| r#" | ||
| INSERT INTO events (community_id, id, pubkey, created_at, kind, tags, content, sig, received_at, channel_id, d_tag, not_before) | ||
| VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) | ||
| INSERT INTO events (community_id, id, pubkey, created_at, kind, tags, content, sig, received_at, channel_id, d_tag, not_before, read_timeline) | ||
| VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) | ||
| ON CONFLICT DO NOTHING | ||
| "#, | ||
| ) | ||
|
|
@@ -1899,6 +1915,7 @@ pub(crate) async fn insert_event_with_thread_metadata_tx( | |
| .bind(channel_id) | ||
| .bind(d_tag.as_deref()) | ||
| .bind(not_before) | ||
| .bind(place.is_some_and(|(_, p)| p.on_timeline())) | ||
| .execute(tx.conn()) | ||
| .await?; | ||
|
|
||
|
|
@@ -1913,6 +1930,12 @@ pub(crate) async fn insert_event_with_thread_metadata_tx( | |
| ) | ||
| .await?; | ||
|
|
||
| // Before the thread counters below: their root row is shared by every | ||
| // reply, so its lock must not be held across these writes. | ||
| if let Some((channel, place)) = place { | ||
| crate::personal_read::record_message(tx, event, channel, place, received_at).await?; | ||
| } | ||
|
|
||
| if let Some(ref meta) = thread_meta { | ||
| let broadcast_val: bool = meta.broadcast; | ||
|
|
||
|
|
@@ -1922,8 +1945,8 @@ pub(crate) async fn insert_event_with_thread_metadata_tx( | |
| (community_id, event_created_at, event_id, channel_id, | ||
| parent_event_id, parent_event_created_at, | ||
| root_event_id, root_event_created_at, | ||
| depth, broadcast) | ||
| VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) | ||
| depth, broadcast, received_at) | ||
| VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) | ||
| ON CONFLICT DO NOTHING | ||
| "#, | ||
| ) | ||
|
|
@@ -1937,6 +1960,8 @@ pub(crate) async fn insert_event_with_thread_metadata_tx( | |
| .bind(meta.root_event_created_at) | ||
| .bind(meta.depth) | ||
| .bind(broadcast_val) | ||
| // An eligible reply's arrival, for the read-state thread index. | ||
| .bind(place.and(meta.parent_event_id).map(|_| received_at)) | ||
| .execute(tx.conn()) | ||
| .await?; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤖 Optional: the new kind is registered in the privacy and provenance gates here, at :205 and in both ingest rejections, but no test exercises those contracts.
Suggested coverage through the real POST and local/Redis fan-out:
Removing any of these registrations should then fail a test.