feat!: remove a relay immediately instead of unpublishing it

Removing a relay now takes effect immediately:

- the profile stops fetching and advertising it,

- secondary devices immediately apply the removal through the transport sync,

Upgrading removes unpublished relays and triggers keyupdates.

BREAKING CHANGE: set_transport_unpublished() is removed: UIs call delete_transport() when the user removes a relay.

BREAKING CHANGE: list_transports_ex() and the TransportListEntry type are removed: use list_transports().

BREAKING CHANGE: delete_transport() no longer refuses to remove the primary transport: it refuses only to remove the last one and re-elects the sending transport as needed.

BREAKING CHANGE: TransportsModified is now also emitted on the device modifying the transports, not only on devices applying the synced change.

Deprecated: DC_STR_PHASING_OUT
This commit is contained in:
holger krekel
2026-08-29 11:30:46 +02:00
parent 3d61e0f349
commit 70a01a6813
33 changed files with 473 additions and 827 deletions

View File

@@ -2623,6 +2623,25 @@ UPDATE msgs SET state=24 WHERE state=18; -- Change OutPreparing to OutFailed.
.await?;
}
inc_and_check(&mut migration_version, 165)?;
if dbversion < migration_version {
// Remove any unpublished relays and cause keyupdates.
sql.execute_migration(
"INSERT INTO removed_transports (addr, remove_timestamp)
SELECT addr, MAX(add_timestamp, unixepoch()) FROM transports
WHERE is_published=0
AND addr!=(SELECT value FROM config WHERE keyname='configured_addr')
ON CONFLICT (addr) DO UPDATE SET
remove_timestamp=MAX(excluded.remove_timestamp, remove_timestamp);
DELETE FROM transports
WHERE is_published=0
AND addr!=(SELECT value FROM config WHERE keyname='configured_addr');
DELETE FROM config WHERE keyname='keyupdate_baseline' AND changes()>0",
migration_version,
)
.await?;
}
let new_version = sql
.get_raw_config_int(VERSION_CFG)
.await?

View File

@@ -35,17 +35,8 @@ async fn test_keyupdate_baseline_migration() -> Result<()> {
let configured = STOP_MIGRATIONS_AT
.scope(163, async move { TestContext::new_alice().await })
.await;
// An address sorting before the primary pins the seed's ORDER BY,
// an unpublished transport pins its filter.
// An address sorting before the existing one pins the seed's ORDER BY.
add_pseudo_transport(&configured, "aa@example.org").await?;
add_pseudo_transport(&configured, "unpublished@example.org").await?;
configured
.sql
.execute(
"UPDATE transports SET is_published=0 WHERE addr='unpublished@example.org'",
(),
)
.await?;
configured.sql.run_migrations(&configured).await?;
let relays = configured.get_config(Config::KeyupdateBaseline).await?;
assert_eq!(relays.as_deref(), Some("aa@example.org alice@example.org"));
@@ -53,6 +44,46 @@ async fn test_keyupdate_baseline_migration() -> Result<()> {
Ok(())
}
/// Tests that upgrading removes unpublished transports.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_unpublished_transport_migration_165() -> Result<()> {
let t = STOP_MIGRATIONS_AT
.scope(163, async move { TestContext::new_alice().await })
.await;
let skewed = tools::time() + 3600;
t.sql
.execute(
"INSERT INTO transports (addr, entered_param, configured_param, is_published, add_timestamp)
VALUES ('unpublished@example.org', '', '', 0, ?)",
(skewed,),
)
.await?;
STOP_MIGRATIONS_AT
.scope(165, async { t.sql.run_migrations(&t).await })
.await?;
assert!(
!t.sql
.exists(
"SELECT COUNT(*) FROM transports WHERE addr='unpublished@example.org'",
(),
)
.await?
);
assert_eq!(
t.sql
.query_get_value(
"SELECT remove_timestamp FROM removed_transports WHERE addr='unpublished@example.org'",
(),
)
.await?,
Some(skewed)
);
assert_eq!(t.get_config(Config::KeyupdateBaseline).await?, None);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_key_contacts_migration_autocrypt() -> Result<()> {
let t = STOP_MIGRATIONS_AT

View File

@@ -1,6 +1,5 @@
use super::*;
use crate::message::Message;
use crate::tools::SystemTime;
use crate::{EventType, test_utils::TestContext};
#[test]
@@ -370,114 +369,3 @@ async fn test_incremental_vacuum() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_remove_unused_hidden_transports() -> Result<()> {
let t = TestContext::new().await;
let now = time();
let transport_id = t
.sql
.transaction(|t| {
// published transport
t.execute(
"INSERT INTO transports
(addr,
entered_param,
configured_param,
add_timestamp)
VALUES (?, ?, ?, ?)",
("1", "", "", now),
)?;
// not published transport
let transport_id = t.query_row::<u32, _, _>(
"INSERT INTO transports
(addr,
entered_param,
configured_param,
is_published,
add_timestamp)
VALUES (?, ?, ?, ?, ?)
RETURNING id",
("2", "", "", 0, now),
|row| row.get(0),
)?;
// this transport has add_timestamp in the future,
// and should not be removed at all, since we only shift time
// by 90 days and one second.
t.execute(
"INSERT INTO transports
(addr,
entered_param,
configured_param,
is_published,
add_timestamp)
VALUES (?, ?, ?, ?, ?)",
("3", "", "", 0, now + 60),
)?;
Ok(transport_id)
})
.await?;
async fn assert_transport_removed(
context: &Context,
hidden_transport_id: u32,
should_be_removed: bool,
) -> Result<()> {
let transports_count: u32 = context
.sql
.query_get_value("SELECT count(*) FROM transports", ())
.await?
.unwrap();
assert_eq!(transports_count, if should_be_removed { 2 } else { 3 });
let hidden_transport_present: bool = context
.sql
.query_get_value(
"SELECT count(*) FROM transports WHERE id=?",
(hidden_transport_id,),
)
.await?
.unwrap();
assert_eq!(hidden_transport_present, !should_be_removed);
Ok(())
}
assert_eq!(
t.sql
.query_get_value::<i64>(
"SELECT last_rcvd_timestamp FROM transports WHERE id=?",
(transport_id,)
)
.await?
.unwrap(),
0
);
update_transport_last_rcvd_timestamp(&t, transport_id).await?;
// last_rcvd_timestamp should update
assert!(
t.sql
.query_get_value::<i64>(
"SELECT last_rcvd_timestamp FROM transports WHERE id=?",
(transport_id,)
)
.await?
.unwrap()
>= now
);
assert_transport_removed(&t, transport_id, false).await?;
remove_unused_hidden_transports(&t).await?;
// it was recently used, so nothing is deleted
assert_transport_removed(&t, transport_id, false).await?;
SystemTime::shift(Duration::from_secs(
(UNPUBLISHED_TRANSPORT_KEEP_TIME + 1).try_into()?,
));
remove_unused_hidden_transports(&t).await?;
assert_transport_removed(&t, transport_id, true).await?;
Ok(())
}