Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ All notable changes to this project will be documented in this file.

### Changes

- CLI
- Drop the activator-only pollers from `doublezero` (user and multicastgroup activation waits). The `--wait` flag on `user create`, `user create-subscribe`, `user subscribe`, `multicastgroup create`, and `multicastgroup update` now fetches the post-create state once instead of polling — creates are atomic to `Activated` post-RFC-11, so the wait loop was watching a transition that no longer happens ([#3614](https://github.com/malbeclabs/doublezero/issues/3614))
- Trim the `Rejected` status arm from the device and link activation pollers; `Rejected` was itself an activator-driven transition ([#3614](https://github.com/malbeclabs/doublezero/issues/3614))
- Client
- Simplify `doublezero connect`'s post-create user fetch to a fixed retry-on-RPC-lag get instead of waiting for `UserStatus::Activated`; the activator-driven transition is gone, so the fetch only needs to ride out replica lag ([#3614](https://github.com/malbeclabs/doublezero/issues/3614))

## [v0.22.0](https://github.com/malbeclabs/doublezero/compare/client/v0.21.0...client/v0.22.0) - 2026-05-08

### Breaking
Expand Down
28 changes: 10 additions & 18 deletions client/doublezero/src/command/connect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -791,40 +791,32 @@ impl ProvisioningCliCommand {
user_pubkey: &Pubkey,
spinner: &ProgressBar,
) -> eyre::Result<User> {
spinner.set_message("Waiting for user activation...");
spinner.set_message("Reading user account...");

// activator polling is done every 1-minute, so if the activator websocket misses the user
// create, then we may need to wait up to 2 minutes for the activator to pick up the user
// User accounts are created atomically in Activated status, but the RPC
// node we read from may lag a few seconds behind the slot the create
// transaction landed in — retry until the account is visible.
let builder = ExponentialBuilder::new()
.with_max_times(8) // 1+2+4+8+16+32+32+32 = 127 seconds max
.with_max_times(6)
.with_min_delay(Duration::from_secs(1))
.with_max_delay(Duration::from_secs(32));
.with_max_delay(Duration::from_secs(8));

let get_activated_user = || {
let get_user = || {
client
.get_user(GetUserCommand {
pubkey: *user_pubkey,
})
.and_then(|(pk, user)| {
if user.status != UserStatus::Activated {
Err(eyre::eyre!("User not activated yet"))
} else {
Ok((pk, user))
}
})
.map_err(|e| eyre::eyre!(e.to_string()))
};

get_activated_user
get_user
.retry(builder)
.notify(|_, dur| {
spinner.set_message(format!(
"Waiting for user activation (checking in {dur:?})..."
))
spinner.set_message(format!("Reading user account (retrying in {dur:?})..."))
})
.call()
.map(|(_, user)| user)
.map_err(|_| eyre::eyre!("Timeout waiting for user activation"))
.map_err(|_| eyre::eyre!("Timeout reading user account"))
}

async fn user_activated<T: ServiceController>(
Expand Down
11 changes: 7 additions & 4 deletions smartcontract/cli/src/multicastgroup/create.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use crate::{
doublezerocommand::CliCommand,
poll_for_activation::poll_for_multicastgroup_activated,
requirements::{CHECK_BALANCE, CHECK_ID_JSON},
validators::{validate_code, validate_parse_bandwidth, validate_pubkey},
};
use clap::Args;
use doublezero_sdk::commands::multicastgroup::create::CreateMulticastGroupCommand;
use doublezero_sdk::commands::multicastgroup::{
create::CreateMulticastGroupCommand, get::GetMulticastGroupCommand,
};
use solana_sdk::pubkey::Pubkey;
use std::{io::Write, str::FromStr};

Expand Down Expand Up @@ -46,8 +47,10 @@ impl CreateMulticastGroupCliCommand {
writeln!(out, "Signature: {signature}",)?;

if self.wait {
let user = poll_for_multicastgroup_activated(client, &pubkey)?;
writeln!(out, "Status: {0}", user.status)?;
let (_, mgroup) = client.get_multicastgroup(GetMulticastGroupCommand {
pubkey_or_code: pubkey.to_string(),
})?;
writeln!(out, "Status: {0}", mgroup.status)?;
}

Ok(())
Expand Down
7 changes: 4 additions & 3 deletions smartcontract/cli/src/multicastgroup/update.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use crate::{
doublezerocommand::CliCommand,
poll_for_activation::poll_for_multicastgroup_activated,
requirements::{CHECK_BALANCE, CHECK_ID_JSON},
validators::{
validate_code, validate_parse_bandwidth, validate_pubkey, validate_pubkey_or_code,
Expand Down Expand Up @@ -67,8 +66,10 @@ impl UpdateMulticastGroupCliCommand {
writeln!(out, "Signature: {signature}",)?;

if self.wait {
let user = poll_for_multicastgroup_activated(client, &pubkey)?;
writeln!(out, "Status: {0}", user.status)?;
let (_, mgroup) = client.get_multicastgroup(GetMulticastGroupCommand {
pubkey_or_code: pubkey.to_string(),
})?;
writeln!(out, "Status: {0}", mgroup.status)?;
}

Ok(())
Expand Down
103 changes: 3 additions & 100 deletions smartcontract/cli/src/poll_for_activation.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
use doublezero_sdk::{
commands::{
device::get::GetDeviceCommand, link::get::GetLinkCommand,
multicastgroup::get::GetMulticastGroupCommand, user::get::GetUserCommand,
},
Device, DeviceStatus, Interface, Link, LinkStatus, MulticastGroup, MulticastGroupStatus, User,
UserStatus,
commands::{device::get::GetDeviceCommand, link::get::GetLinkCommand},
Device, DeviceStatus, Interface, Link, LinkStatus,
};
use solana_sdk::pubkey::Pubkey;

Expand Down Expand Up @@ -40,7 +36,6 @@ pub fn poll_for_device_activated(
Ok((_, device)) => {
if device.status == DeviceStatus::DeviceProvisioning
|| device.status == DeviceStatus::Activated
|| device.status == DeviceStatus::Rejected
{
return Ok(device);
}
Expand Down Expand Up @@ -133,10 +128,7 @@ pub fn poll_for_link_activated(
pubkey_or_code: link_pubkey.to_string(),
}) {
Ok((_, link)) => {
if link.status == LinkStatus::Provisioning
|| link.status == LinkStatus::Activated
|| link.status == LinkStatus::Rejected
{
if link.status == LinkStatus::Provisioning || link.status == LinkStatus::Activated {
return Ok(link);
}
}
Expand All @@ -151,92 +143,3 @@ pub fn poll_for_link_activated(
std::thread::sleep(poll_interval);
}
}

pub fn poll_for_user_activated(
client: &dyn CliCommand,
user_pubkey: &Pubkey,
) -> eyre::Result<User> {
let start_time = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(60);
let poll_interval = std::time::Duration::from_secs(1);
let mut last_error: Option<eyre::Error> = None;

loop {
if start_time.elapsed() >= timeout {
return Err(match last_error {
Some(e) => eyre::eyre!(
"Timeout waiting for user activation after {} seconds. Last error: {}",
timeout.as_secs(),
e
),
None => eyre::eyre!(
"Timeout waiting for user activation after {} seconds",
timeout.as_secs()
),
});
}

match client.get_user(GetUserCommand {
pubkey: *user_pubkey,
}) {
Ok((_, user)) => {
if user.status == UserStatus::Activated || user.status == UserStatus::Rejected {
return Ok(user);
}
}
Err(e) => {
// User not found or some other error, continue polling
// It may take some time for the user to be visible onchain after the creation
// transaction is confirmed, so we need to poll here until is is.
last_error = Some(e);
}
}

std::thread::sleep(poll_interval);
}
}

pub fn poll_for_multicastgroup_activated(
client: &dyn CliCommand,
mgroup_pubkey: &Pubkey,
) -> eyre::Result<MulticastGroup> {
let start_time = std::time::Instant::now();
let timeout = std::time::Duration::from_secs(60);
let poll_interval = std::time::Duration::from_secs(1);
let mut last_error: Option<eyre::Error> = None;

loop {
if start_time.elapsed() >= timeout {
return Err(match last_error {
Some(e) => eyre::eyre!(
"Timeout waiting for multicast group activation after {} seconds. Last error: {}",
timeout.as_secs(),
e
),
None => eyre::eyre!("Timeout waiting for multicast group activation after {} seconds",
timeout.as_secs()
),
});
}

match client.get_multicastgroup(GetMulticastGroupCommand {
pubkey_or_code: mgroup_pubkey.to_string(),
}) {
Ok((_, mgroup)) => {
if mgroup.status == MulticastGroupStatus::Activated
|| mgroup.status == MulticastGroupStatus::Rejected
{
return Ok(mgroup);
}
}
Err(e) => {
// Multicast group not found or some other error, continue polling
// It may take some time for the multicast group to be visible onchain after the creation
// transaction is confirmed, so we need to poll here until is is.
last_error = Some(e);
}
}

std::thread::sleep(poll_interval);
}
}
9 changes: 5 additions & 4 deletions smartcontract/cli/src/user/create.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
use crate::{
doublezerocommand::CliCommand,
helpers::parse_pubkey,
poll_for_activation::poll_for_user_activated,
requirements::{CHECK_BALANCE, CHECK_ID_JSON},
validators::validate_pubkey_or_code,
};
use clap::Args;
use doublezero_sdk::{
commands::{
accesspass::get::GetAccessPassCommand, device::get::GetDeviceCommand,
tenant::get::GetTenantCommand, user::create::CreateUserCommand,
accesspass::get::GetAccessPassCommand,
device::get::GetDeviceCommand,
tenant::get::GetTenantCommand,
user::{create::CreateUserCommand, get::GetUserCommand},
},
UserCYOA, UserType,
};
Expand Down Expand Up @@ -105,7 +106,7 @@ impl CreateUserCliCommand {
writeln!(out, "Signature: {signature}",)?;

if self.wait {
let user = poll_for_user_activated(client, &pubkey)?;
let (_, user) = client.get_user(GetUserCommand { pubkey })?;
writeln!(out, "Status: {0}", user.status)?;
}

Expand Down
8 changes: 4 additions & 4 deletions smartcontract/cli/src/user/create_subscribe.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
use crate::{
doublezerocommand::CliCommand,
helpers::parse_pubkey,
poll_for_activation::poll_for_user_activated,
requirements::{CHECK_BALANCE, CHECK_ID_JSON},
validators::validate_pubkey_or_code,
};
use clap::Args;
use doublezero_sdk::{
commands::{
device::get::GetDeviceCommand, multicastgroup::get::GetMulticastGroupCommand,
user::create_subscribe::CreateSubscribeUserCommand,
device::get::GetDeviceCommand,
multicastgroup::get::GetMulticastGroupCommand,
user::{create_subscribe::CreateSubscribeUserCommand, get::GetUserCommand},
},
*,
};
Expand Down Expand Up @@ -109,7 +109,7 @@ impl CreateSubscribeUserCliCommand {
writeln!(out, "Signature: {signature}",)?;

if self.wait {
let user = poll_for_user_activated(client, &pubkey)?;
let (_, user) = client.get_user(GetUserCommand { pubkey })?;
writeln!(out, "Status: {0}", user.status)?;
}

Expand Down
7 changes: 4 additions & 3 deletions smartcontract/cli/src/user/subscribe.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use crate::{
doublezerocommand::CliCommand,
helpers::parse_pubkey,
poll_for_activation::{poll_for_multicastgroup_activated, poll_for_user_activated},
requirements::{CHECK_BALANCE, CHECK_ID_JSON},
validators::{validate_pubkey, validate_pubkey_or_code},
};
Expand Down Expand Up @@ -71,10 +70,12 @@ impl SubscribeUserCliCommand {
}

if self.wait {
let user = poll_for_user_activated(client, &user_pk)?;
let (_, user) = client.get_user(GetUserCommand { pubkey: user_pk })?;
writeln!(out, "User status: {}", user.status)?;
for group_pk in &group_pks {
let mgroup = poll_for_multicastgroup_activated(client, group_pk)?;
let (_, mgroup) = client.get_multicastgroup(GetMulticastGroupCommand {
pubkey_or_code: group_pk.to_string(),
})?;
writeln!(out, "Multicast group {group_pk} status: {}", mgroup.status)?;
}
}
Expand Down
Loading