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: support using a shared sandbox instance #71

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
9 changes: 7 additions & 2 deletions workspaces/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ Library for automating workflows and testing NEAR smart contracts.
"""

[dependencies]
async-mutex = "1.4.0"
async-trait = "0.1"
anyhow = "1.0"
base64 = "0.13"
borsh = "0.9"
chrono = "0.4.19"
dirs = "3.0.2"
hex = "0.4.2"
once_cell = "1.9.0"
portpicker = "0.1.1"
rand = "0.8.4"
reqwest = { version = "0.11", features = ["json"] }
Expand All @@ -31,10 +33,10 @@ near-crypto = "0.12.0"
near-primitives = "0.12.0"
near-jsonrpc-primitives = "0.12.0"
near-jsonrpc-client = { version = "0.3.0", features = ["sandbox"] }
near-sandbox-utils = "0.1.1"
near-sandbox-utils = { git = "https://github.com/near/sandbox", branch = "daniyar/autocloseable" }

[build-dependencies]
near-sandbox-utils = "0.1"
near-sandbox-utils = { git = "https://github.com/near/sandbox", branch = "daniyar/autocloseable" }

[target.'cfg(unix)'.dependencies]
libc = "0.2"
Expand All @@ -43,3 +45,6 @@ libc = "0.2"
borsh = "0.9"
test-log = { version = "0.2.8", default-features = false, features = ["trace"] }
tracing-subscriber = { version = "0.3.5", features = ["env-filter"] }

[features]
sandbox-parallel = []
84 changes: 69 additions & 15 deletions workspaces/src/network/sandbox.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,43 @@
use std::path::PathBuf;
use std::str::FromStr;

use async_trait::async_trait;

use super::{
Account, AllowDevAccountCreation, AllowStatePatching, CallExecution, Contract, NetworkClient,
NetworkInfo, TopLevelAccountCreator,
};

use crate::network::server::SandboxServer;
use crate::network::Info;
use crate::rpc::client::Client;
use crate::types::{AccountId, Balance, InMemorySigner, SecretKey};
use async_mutex::Mutex;
use async_trait::async_trait;
use once_cell::sync::Lazy;
use std::future::Future;
use std::path::PathBuf;
use std::str::FromStr;

#[cfg(not(feature = "sandbox-parallel"))]
static SHARED_SERVER: Lazy<SandboxServer> = Lazy::new(|| {
let mut server = SandboxServer::default();
server.start().unwrap();
server
});

// Using a shared sandbox instance is thread-safe as long as all threads use it with their own
// account. This means, however, is that the creation of these accounts should be sequential in
// order to avoid duplicated nonces.
#[cfg(not(feature = "sandbox-parallel"))]
static TLA_ACCOUNT_MUTEX: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));

// Constant taken from nearcore crate to avoid dependency
pub(crate) const NEAR_BASE: Balance = 1_000_000_000_000_000_000_000_000;

const DEFAULT_DEPOSIT: Balance = 100 * NEAR_BASE;
pub(crate) const DEFAULT_DEPOSIT: Balance = 100 * NEAR_BASE;

#[cfg(not(feature = "sandbox-parallel"))]
pub struct Sandbox {
client: Client,
info: Info,
}

#[cfg(feature = "sandbox-parallel")]
pub struct Sandbox {
server: SandboxServer,
client: Client,
Expand All @@ -31,13 +51,36 @@ impl Sandbox {
path
}

#[cfg(not(feature = "sandbox-parallel"))]
pub(crate) fn root_signer(&self) -> InMemorySigner {
let mut path = Self::home_dir(SHARED_SERVER.rpc_port);
path.push("validator_key.json");

InMemorySigner::from_file(&path)
}

#[cfg(feature = "sandbox-parallel")]
pub(crate) fn root_signer(&self) -> InMemorySigner {
let mut path = Self::home_dir(self.server.rpc_port);
path.push("validator_key.json");

InMemorySigner::from_file(&path)
}

#[cfg(not(feature = "sandbox-parallel"))]
pub(crate) fn new() -> Self {
let client = Client::new(SHARED_SERVER.rpc_addr());
let info = Info {
name: "sandbox-shared".to_string(),
root_id: AccountId::from_str("test.near").unwrap(),
keystore_path: PathBuf::from(".near-credentials/sandbox/"),
rpc_url: SHARED_SERVER.rpc_addr(),
};

Self { client, info }
}

#[cfg(feature = "sandbox-parallel")]
pub(crate) fn new() -> Self {
let mut server = SandboxServer::default();
server.start().unwrap();
Expand All @@ -62,6 +105,15 @@ impl AllowStatePatching for Sandbox {}

impl AllowDevAccountCreation for Sandbox {}

async fn tla_guarded<F: FnOnce() -> Fut, Fut: Future<Output = T>, T>(f: F) -> T {
#[cfg(not(feature = "sandbox-parallel"))]
let guard = TLA_ACCOUNT_MUTEX.lock().await;
let result = f().await;
#[cfg(not(feature = "sandbox-parallel"))]
drop(guard);
result
}

#[async_trait]
impl TopLevelAccountCreator for Sandbox {
async fn create_tla(
Expand All @@ -70,10 +122,12 @@ impl TopLevelAccountCreator for Sandbox {
sk: SecretKey,
) -> anyhow::Result<CallExecution<Account>> {
let root_signer = self.root_signer();
let outcome = self
.client
.create_account(&root_signer, &id, sk.public_key(), DEFAULT_DEPOSIT)
.await?;

let outcome = tla_guarded(|| {
self.client
.create_account(&root_signer, &id, sk.public_key(), DEFAULT_DEPOSIT)
})
.await?;

let signer = InMemorySigner::from_secret_key(id.clone(), sk);
Ok(CallExecution {
Expand All @@ -89,16 +143,16 @@ impl TopLevelAccountCreator for Sandbox {
wasm: &[u8],
) -> anyhow::Result<CallExecution<Contract>> {
let root_signer = self.root_signer();
let outcome = self
.client
.create_account_and_deploy(
let outcome = tla_guarded(|| {
self.client.create_account_and_deploy(
&root_signer,
&id,
sk.public_key(),
DEFAULT_DEPOSIT,
wasm.into(),
)
.await?;
})
.await?;

let signer = InMemorySigner::from_secret_key(id.clone(), sk);
Ok(CallExecution {
Expand Down
16 changes: 8 additions & 8 deletions workspaces/src/network/server.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
use crate::network::Sandbox;
use near_sandbox_utils::SandboxHandle;
use portpicker::pick_unused_port;
use std::process::Child;
use tracing::info;

pub struct SandboxServer {
pub(crate) rpc_port: u16,
pub(crate) net_port: u16,
process: Option<Child>,
sandbox_handle: Option<SandboxHandle>,
}

impl SandboxServer {
pub fn new(rpc_port: u16, net_port: u16) -> Self {
Self {
rpc_port,
net_port,
process: None,
sandbox_handle: None,
}
}

Expand All @@ -26,9 +26,9 @@ impl SandboxServer {
let _ = std::fs::remove_dir_all(&home_dir);
near_sandbox_utils::init(&home_dir)?.wait()?;

let child = near_sandbox_utils::run(&home_dir, self.rpc_port, self.net_port)?;
info!(target: "workspaces", "Started sandbox: pid={:?}", child.id());
self.process = Some(child);
let sandbox_handle = near_sandbox_utils::run(&home_dir, self.rpc_port, self.net_port)?;
info!(target: "workspaces", "Started sandbox: pid={:?}", sandbox_handle.sandbox_process.id());
self.sandbox_handle = Some(sandbox_handle);

Ok(())
}
Expand All @@ -48,11 +48,11 @@ impl Default for SandboxServer {

impl Drop for SandboxServer {
fn drop(&mut self) {
if self.process.is_none() {
if self.sandbox_handle.is_none() {
return;
}

let child = self.process.as_mut().unwrap();
let child = &mut self.sandbox_handle.as_mut().unwrap().sandbox_process;

info!(
target: "workspaces",
Expand Down
14 changes: 12 additions & 2 deletions workspaces/src/rpc/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,12 @@ impl Client {
near_jsonrpc_primitives::types::transactions::RpcTransactionError,
> {
retry(|| async {
let result = JsonRpcClient::connect(&self.rpc_addr).call(method).await;
// A new client is required since using a shared one between different threads can
// cause https://github.com/hyperium/hyper/issues/2112
let result = JsonRpcClient::new_client()
.connect(&self.rpc_addr)
.call(method)
.await;
match &result {
Ok(response) => {
// When user sets logging level to INFO we only print one-liners with submitted
Expand Down Expand Up @@ -89,7 +94,12 @@ impl Client {
M::Error: Debug,
{
retry(|| async {
let result = JsonRpcClient::connect(&self.rpc_addr).call(method).await;
// A new client is required since using a shared one between different threads can
// cause https://github.com/hyperium/hyper/issues/2112
let result = JsonRpcClient::new_client()
.connect(&self.rpc_addr)
.call(method)
.await;
tracing::debug!(
target: "workspaces",
"Querying RPC with {:?} resulted in {:?}",
Expand Down
8 changes: 3 additions & 5 deletions workspaces/src/worker/impls.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
use std::collections::HashMap;

use async_trait::async_trait;
use near_primitives::types::{Balance, StoreKey};

use crate::network::{
Account, AllowDevAccountCreation, CallExecution, CallExecutionDetails, Contract, NetworkClient,
NetworkInfo, StatePatcher, TopLevelAccountCreator, ViewResultDetails,
Expand All @@ -13,6 +8,9 @@ use crate::rpc::patch::ImportContractBuilder;
use crate::types::{AccountId, Gas, InMemorySigner, SecretKey};
use crate::worker::Worker;
use crate::Network;
use async_trait::async_trait;
use near_primitives::types::{Balance, StoreKey};
use std::collections::HashMap;

impl<T> Clone for Worker<T> {
fn clone(&self) -> Self {
Expand Down