n0-computer / iroh

A toolkit for building distributed applications
https://iroh.computer
Apache License 2.0
2.4k stars 155 forks source link

Err value: Open(RemoteDropped) #2477

Open azzamsa opened 3 months ago

azzamsa commented 3 months ago

Hi, 👋

I can reproduce called Result::unwrap() on an Err value: Open(RemoteDropped), every time I run the code below.

use std::str::FromStr;

use iroh::blobs::store::fs::Store;
use iroh::docs::NamespaceSecret;
use iroh::{base::base32, client::docs::Entry, docs::store::Query, node::Node};

use clap::{Parser, Subcommand};
use indicatif::HumanBytes;
use iroh::client::Doc;
use rand::Rng;
use tokio_stream::StreamExt;

pub static GLOBAL_NAMESPACE: &str = "q4hiommvh3ttec3x2y7h4le5tkx2tee762s6miu4rer6d2asi4la";

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    run().await?;
    Ok(())
}

async fn run() -> anyhow::Result<()> {
    let opts = Opts::parse();
    let storage_path = std::env::current_dir().unwrap().join("data");
    tokio::fs::create_dir_all(&storage_path).await?;

    // Initialize node
    let node = Node::persistent(&storage_path).await?.spawn().await?;

    // Get author, `authors().default()` doesn't work
    let author_id = if let Some(first_author) = node.client().authors().list().await?.next().await {
        first_author?
    } else {
        node.client().authors().create().await?
    };

    match opts.cmd.as_ref() {
        Some(Command::Add { description }) => {
            let document = get_document(node).await?;

            let todo_id = gen_id().to_string();
            println!("- [ ] {}: {}", &todo_id, &description);

            // Write
            document
                .set_bytes(author_id, todo_id, description.to_owned())
                .await?;
            let document_id = document.id().to_string();

            println!(
                r#"
            document id: {}
            author   id: {}"#,
                &document_id, author_id
            );
        }
        None => {
            let document = get_document(node).await?;
            let document_id = document.id().to_string();

            println!("::: Listing entry for document_id: {document_id}");
            let mut stream = document.get_many(Query::all()).await.unwrap();
            while let Some(entry) = stream.try_next().await.unwrap() {
                println!("entry {}", fmt_entry(&entry));
                let content = entry.content_bytes(&document).await.unwrap();
                println!("  content {}", std::str::from_utf8(&content).unwrap())
            }
        }
    }

    Ok(())
}

async fn get_document(node: Node<Store>) -> anyhow::Result<Doc> {
    let document = node
        .client()
        .docs()
        .import_namespace(iroh::docs::Capability::Write(
            NamespaceSecret::from_str(GLOBAL_NAMESPACE).unwrap(),
        ))
        .await?;
    println!("::: Using namespace '{GLOBAL_NAMESPACE}'");
    Ok(document)
}

fn fmt_entry(entry: &Entry) -> String {
    let id = entry.id();
    let key = std::str::from_utf8(id.key()).unwrap_or("<bad key>");
    let author = id.author().fmt_short();
    let hash = entry.content_hash();
    let hash = base32::fmt_short(hash.as_bytes());
    let len = HumanBytes(entry.content_len());
    format!("@{author}: {key} = {hash} ({len})",)
}

fn gen_id() -> i32 {
    let mut rng = rand::thread_rng();
    rng.gen_range(1..=90)
}

#[derive(Parser)]
#[command(name = "todos", version)]
pub struct Opts {
    #[command(subcommand)]
    pub cmd: Option<Command>,
}

#[derive(Subcommand)]
pub enum Command {
    /// Add a new todo task
    Add {
        #[arg(short, long)]
        description: String,
    },
}
[dependencies]
tokio = { version = "1.38.0", features = ["macros", "rt-multi-thread"] }
tokio-stream = "0.1.15"

iroh = "0.19.0"

anyhow = "1.0.86"
thiserror = "1.0"
clap = { version = "4.5.8", features = ["derive"] }
rand = "0.8.5"
indicatif = "0.17.8"
iroh-blobs = "0.19.0"

@zicklag gives the fix for me and explains that:

So the RemoteDropped happens because get_document() takes an owned Node. After get_document() exits, the node is dropped, because it's moved into the function. The document that is returned is actually just like a handle to a connection to the node, so once the node's dropped, the document can't actually get any data. If you make get_document() take a &Node instead, it works. BTW, instead of passing around &Node I think it's easier to pass around an Iroh client, which you can get with node.client(). The Iroh client can be cloned around and harmlessly dropped, since it's just holding a connection to the Iroh node.

This is the patch that was implemented based on the @zicklag guide.

modified src/main.rs
@@ -1,12 +1,14 @@
 use std::str::FromStr;

-use iroh::blobs::store::fs::Store;
-use iroh::docs::NamespaceSecret;
-use iroh::{base::base32, client::docs::Entry, docs::store::Query, node::Node};
+use iroh::{
+    base::base32,
+    client::{docs::Entry, Doc, Iroh},
+    docs::{store::Query, NamespaceSecret},
+    node::Node,
+};

 use clap::{Parser, Subcommand};
 use indicatif::HumanBytes;
-use iroh::client::Doc;
 use rand::Rng;
 use tokio_stream::StreamExt;

@@ -25,17 +27,18 @@ async fn run() -> anyhow::Result<()> {

     // Initialize node
     let node = Node::persistent(&storage_path).await?.spawn().await?;
+    let client = node.client();

     // Get author, `authors().default()` doesn't work
     let author_id = if let Some(first_author) = node.client().authors().list().await?.next().await {
         first_author?
     } else {
-        node.client().authors().create().await?
+        client.authors().create().await?
     };

     match opts.cmd.as_ref() {
         Some(Command::Add { description }) => {
-            let document = get_document(node).await?;
+            let document = get_document(client).await?;

             let todo_id = gen_id().to_string();
             println!("- [ ] {}: {}", &todo_id, &description);
@@ -46,15 +49,11 @@ async fn run() -> anyhow::Result<()> {
                 .await?;
             let document_id = document.id().to_string();

         }
         None => {
-            let document = get_document(node).await?;
+            let document = get_document(client).await?;
             let document_id = document.id().to_string();

             println!("::: Listing entry for document_id: {document_id}");
@@ -70,9 +69,8 @@ async fn run() -> anyhow::Result<()> {
     Ok(())
 }

-async fn get_document(node: Node<Store>) -> anyhow::Result<Doc> {
-    let document = node
-        .client()
+async fn get_document(client: &Iroh) -> anyhow::Result<Doc> {
+    let document = client
         .docs()
         .import_namespace(iroh::docs::Capability::Write(
             NamespaceSecret::from_str(GLOBAL_NAMESPACE).unwrap(),

Is there any way for Rust or Iroh to prevent this Open(RemoteDropped) error?

matheus23 commented 2 months ago

Thanks! I hope this helps others searching for the same error message.

There is a way to prevent this error, but it'd mean changing the API and would need some thought. Here are some ideas:

zicklag commented 2 months ago

@matheus23 what do you think about simply printing a warning message if the node is dropped without being shutdown?

To me the behavior seems reasonable as it is, just difficult to discover.

matheus23 commented 2 months ago

@zicklag Hm. Yeah that's probably better. Personally I'd like to think about if we can make this better still. E.g. perhaps we can change the error message from Err(Open(RemoteDropped)) to something that helps the user by saying "Error: Cannot access node, it was possibly shut down or dropped already".