Skip to content
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

feat: dfx start doesn't require --clean when changing replica versions or options #3777

Merged
merged 11 commits into from
May 31, 2024
43 changes: 43 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,49 @@

# UNRELEASED

### feat: `dfx start` for the shared local network stores replica state files in unique directories by options

The state files for different replica versions are often incompatible,
so `dfx start` requires the `--clean` argument in order to reset data when
using different replica versions or different replica options.

For the local shared network, dfx now stores replica state files in different
directories, split up by replica version and options.

As an example, you'll be able to do things like this going forward:
```bash
dfx +0.21.0 start
(cd project1 && dfx deploy && dfx canister call ...)
dfx stop

dfx +0.22.0 start
# notice --clean is not required.
# even if --clean were passed, the canisters for project1 would be unaffected.
(cd project2 && dfx deploy)
# project1 won't be affected unless you call dfx in its directory
dfx stop

dfx +0.21.0 start
# the canisters are still deployed
(cd project1 && dfx canister call ...)
```

Prior to this change, the second `dfx start` would have had to include `--clean`,
which would have reset the state of the shared local network, affecting all projects.

This also means `dfx start` for the shared local network won't ever require you to pass `--clean`.

`dfx start` will delete old replica state directories. At present, it retains the 10 most recently used.

This doesn't apply to project-specific networks, and it doesn't apply with `--pocketic`.

It doesn't apply to project-specific networks because the project's canister ids would
reset anyway on first access. If you run `dfx start` in a project directory where dfx.json
defines the local network, you'll still be prompted to run with `--clean` if using a
different replica version or different replica options.

It doesn't apply to `--pocketic` because PocketIC does not yet persist any data.

# 0.20.2

### fix: `dfx canister delete` fails
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions e2e/tests-dfx/shared_local_network.bash
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ teardown() {
standard_teardown
}

@test "shared local network always requires --clean with pocketic" {
[[ "$USE_POCKETIC" ]] || skip "specific to dfx start --pocketic"

assert_command dfx_start
assert_command dfx stop

assert_command_fail dfx start --pocketic
assert_contains "The network state can't be reused with this configuration. Rerun with \`--clean\`"
}

@test "dfx start creates no files in the current directory when run from an empty directory" {
dfx_start
assert_command find .
Expand Down
52 changes: 45 additions & 7 deletions e2e/tests-dfx/start.bash
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,45 @@ teardown() {
standard_teardown
}

@test "start and stop with different options" {
[[ "$USE_POCKETIC" ]] && skip "skipped for pocketic: artificial delay, and clean required"
dfx_start --artificial-delay 101
dfx_stop

# notice: no need to --clean
dfx_start --artificial-delay 102
dfx_stop
}

@test "project networks still need --clean" {
[[ "$USE_POCKETIC" ]] && skip "skipped for pocketic: artificial delay"
dfx_new hello
define_project_network

dfx_start --artificial-delay 101
dfx stop

assert_command_fail dfx_start --artificial-delay 102
}

@test "stop and start with other options does not disrupt projects" {
[[ "$USE_POCKETIC" ]] && skip "skipped for pocketic: artificial delay"
dfx_start --artificial-delay 101

dfx_new p1
assert_command dfx deploy
CANISTER_ID="$(dfx canister id p1_backend)"

assert_command dfx stop
dfx_start --artificial-delay 102
assert_command dfx stop

dfx_start --artificial-delay 101

assert_command dfx canister id p1_backend
assert_eq "$CANISTER_ID"
}

@test "start and stop outside project" {
dfx_start

Expand Down Expand Up @@ -447,18 +486,14 @@ teardown() {
assert_match "Hello, World! from DFINITY"
}

@test "modifying networks.json requires --clean on restart" {
@test "modifying networks.json does not require --clean on restart" {
[[ "$USE_POCKETIC" ]] && skip "skipped for pocketic: --force"
dfx_start
dfx stop
assert_command dfx_start
dfx stop
jq -n '.local.replica.log_level="warning"' > "$E2E_NETWORKS_JSON"
assert_command_fail dfx_start
assert_contains "The network state can't be reused with this configuration. Rerun with \`--clean\`."
assert_command dfx_start --force
dfx stop
assert_command dfx_start --clean
assert_command dfx_start
}

@test "project-local networks require --clean if dfx.json was updated" {
Expand All @@ -480,8 +515,11 @@ teardown() {
assert_command dfx_start --clean
}

@test "flags count as configuration modification and require --clean" {
@test "flags count as configuration modification and require --clean for a project network" {
[[ "$USE_POCKETIC" ]] && skip "skipped for pocketic: --artificial-delay"
dfx_new
define_project_network

dfx start --background
dfx stop
assert_command_fail dfx start --artificial-delay 100 --background
Expand Down
1 change: 1 addition & 0 deletions src/dfx-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ sec1 = { workspace = true, features = ["std"] }
semver = { workspace = true, features = ["serde"] }
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
slog = { workspace = true, features = ["max_level_trace"] }
tar.workspace = true
tempfile.workspace = true
Expand Down
31 changes: 29 additions & 2 deletions src/dfx-core/src/config/model/local_server_descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub struct LocalServerDescriptor {
/// $HOME/.local/share/dfx/network/local
/// $APPDATA/dfx/network/local
pub data_directory: PathBuf,
pub settings_digest: Option<String>,

pub bind_address: SocketAddr,

Expand Down Expand Up @@ -62,9 +63,11 @@ impl LocalServerDescriptor {
scope: LocalNetworkScopeDescriptor,
legacy_pid_path: Option<PathBuf>,
) -> Result<Self, NetworkConfigError> {
let settings_digest = None;
let bind_address = to_socket_addr(&bind).map_err(ParseBindAddressFailed)?;
Ok(LocalServerDescriptor {
data_directory,
settings_digest,
bind_address,
bitcoin,
canister_http,
Expand All @@ -78,7 +81,7 @@ impl LocalServerDescriptor {
/// The contents of this file are different for each `dfx start --clean`
/// or `dfx start` when the network data directory doesn't already exist
pub fn network_id_path(&self) -> PathBuf {
self.data_directory.join("network-id")
self.data_dir_by_settings_digest().join("network-id")
}

/// This file contains the pid of the process started with `dfx start`
Expand Down Expand Up @@ -164,9 +167,21 @@ impl LocalServerDescriptor {
path.exists().then(|| load_json_file(&path)).transpose()
}

pub fn data_dir_by_settings_digest(&self) -> PathBuf {
if self.scope == LocalNetworkScopeDescriptor::Project {
self.data_directory.clone()
} else {
let settings_digest = self
.settings_digest
.as_ref()
.expect("settings_digest must be set");
self.data_directory.join(settings_digest)
}
}

/// The top-level directory holding state for the replica.
pub fn state_dir(&self) -> PathBuf {
self.data_directory.join("state")
self.data_dir_by_settings_digest().join("state")
}

/// The replicated state of the replica.
Expand All @@ -184,6 +199,11 @@ impl LocalServerDescriptor {
pub fn effective_config_path(&self) -> PathBuf {
self.data_directory.join("replica-effective-config.json")
}

pub fn effective_config_path_by_settings_digest(&self) -> PathBuf {
self.data_dir_by_settings_digest()
.join("replica-effective-config.json")
}
}

impl LocalServerDescriptor {
Expand Down Expand Up @@ -224,6 +244,13 @@ impl LocalServerDescriptor {
};
Self { proxy, ..self }
}

pub fn with_settings_digest(self, settings_digest: String) -> Self {
Self {
settings_digest: Some(settings_digest),
..self
}
}
}

impl LocalServerDescriptor {
Expand Down
1 change: 1 addition & 0 deletions src/dfx-core/src/config/model/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ pub mod extension_canister_type;
pub mod local_server_descriptor;
pub mod network_descriptor;
pub mod replica_config;
pub mod settings_digest;
4 changes: 2 additions & 2 deletions src/dfx-core/src/config/model/replica_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,15 +184,15 @@ impl HttpHandlerConfig {
}
}

#[derive(Serialize, Deserialize, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
#[allow(clippy::large_enum_variant)]
pub enum CachedReplicaConfig<'a> {
Replica { config: Cow<'a, ReplicaConfig> },
PocketIc,
}

#[derive(Serialize, Deserialize, PartialEq, Eq)]
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct CachedConfig<'a> {
pub replica_rev: String,
#[serde(flatten)]
Expand Down
115 changes: 115 additions & 0 deletions src/dfx-core/src/config/model/settings_digest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use crate::config::model::dfinity::{ReplicaLogLevel, ReplicaSubnetType};
use crate::config::model::local_server_descriptor::LocalServerDescriptor;
use candid::Deserialize;
use serde::Serialize;
use sha2::{Digest, Sha256};
use std::borrow::Cow;

#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
enum HttpHandlerPortSetting {
Port {
port: u16,
},
#[default]
WritePortToPath,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
struct HttpHandlerSettings {
pub port: HttpHandlerPortSetting,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
struct BtcAdapterSettings {
pub enabled: bool,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
struct CanisterHttpAdapterSettings {
pub enabled: bool,
}

#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
struct ReplicaSettings {
pub http_handler: HttpHandlerSettings,
pub subnet_type: ReplicaSubnetType,
pub btc_adapter: BtcAdapterSettings,
pub canister_http_adapter: CanisterHttpAdapterSettings,
pub log_level: ReplicaLogLevel,
pub artificial_delay: u32,
pub use_old_metering: bool,
}

#[derive(Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
enum BackendSettings<'a> {
Replica { settings: Cow<'a, ReplicaSettings> },
PocketIc,
}

#[derive(Serialize, Deserialize, PartialEq, Eq)]
struct Settings<'a> {
pub ic_repo_commit: String,
#[serde(flatten)]
pub backend: BackendSettings<'a>,
}

pub fn get_settings_digest(
ic_repo_commit: &str,
local_server_descriptor: &LocalServerDescriptor,
use_old_metering: bool,
artificial_delay: u32,
pocketic: bool,
) -> String {
let backend = if pocketic {
BackendSettings::PocketIc
} else {
get_replica_backend_settings(local_server_descriptor, use_old_metering, artificial_delay)
};
let settings = Settings {
ic_repo_commit: ic_repo_commit.into(),
backend,
};
let normalized = serde_json::to_string_pretty(&settings).unwrap();
let hash: Vec<u8> = Sha256::digest(normalized).to_vec();
hex::encode(hash)
}

fn get_replica_backend_settings(
local_server_descriptor: &LocalServerDescriptor,
use_old_metering: bool,
artificial_delay: u32,
) -> BackendSettings {
let http_handler = HttpHandlerSettings {
port: if let Some(port) = local_server_descriptor.replica.port {
HttpHandlerPortSetting::Port { port }
} else {
HttpHandlerPortSetting::WritePortToPath
},
};
let btc_adapter = BtcAdapterSettings {
enabled: local_server_descriptor.bitcoin.enabled,
};
let canister_http_adapter = CanisterHttpAdapterSettings {
enabled: local_server_descriptor.canister_http.enabled,
};
let replica_settings = ReplicaSettings {
http_handler,
subnet_type: local_server_descriptor
.replica
.subnet_type
.unwrap_or_default(),
btc_adapter,
canister_http_adapter,
log_level: local_server_descriptor
.replica
.log_level
.unwrap_or_default(),
artificial_delay,
use_old_metering,
};
BackendSettings::Replica {
settings: Cow::Owned(replica_settings),
}
}
Loading
Loading