Cyclic reference structs implementing traits in an async context

Hi everyone,

I'm trying to implement cyclic references between 3 struct which implementsProvider, Namespace, and Node traits, these struct can have different implementation, to keep it simple we will start with 1 implementation Kubernetes, which gives KubernetesProvider, KubernetesNamespace and KubernetesNode, the traits are defined as follow:

#[async_trait]
trait Provider {
    async fn create_namespace(&self) -> Arc<dyn Namespace>;
}

#[async_trait]
trait Namespace {
    async fn create_node(&self) -> Arc<dyn Node>;
    async fn destroy(&self);
}

#[async_trait]
trait Node {
    async fn destroy(&self);
}

and the struct for Kubernetes are defined as follow:

struct KubernetesProvider {
    namespaces: RwLock<HashMap<String, Arc<KubernetesNamespace>>>,
    provider_field: String,
}

impl KubernetesProvider {
    fn new() -> Arc<Self> {
        Arc::new(KubernetesProvider {
            namespaces: RwLock::new(HashMap::new()),
            provider_field: "provider_field".to_string(),
        })
    }
}

#[async_trait]
impl Provider for KubernetesProvider {
    async fn create_namespace(&self) -> Arc<dyn Namespace> {
        let name = format!("namespace_{}", Uuid::new_v4());
        let namespace = KubernetesNamespace::new(self, &name);

        self.namespaces
            .write()
            .await
            .insert(name, namespace.clone());

        namespace
    }
}

struct KubernetesNamespace {
    provider: Weak<KubernetesProvider>,
    name: String,
    nodes: RwLock<HashMap<String, Arc<KubernetesNode>>>,
}

impl KubernetesNamespace {
    fn new(provider: &Arc<KubernetesProvider>, name: &str) -> Arc<Self> {
        Arc::new(KubernetesNamespace {
            provider: Arc::downgrade(provider),
            name: name.to_string(),
            nodes: RwLock::new(HashMap::new()),
        })
    }
}

#[async_trait]
impl Namespace for KubernetesNamespace {
    async fn create_node(&self) -> Arc<dyn Node> {
        let name = format!("node_{}", Uuid::new_v4());
        let node = KubernetesNode::new(self, &name);

        self.nodes.write().await.insert(name, node.clone());

        node
    }

    async fn destroy(&self) {
        if let Some(provider) = self.provider.upgrade() {
            provider.namespaces.write().await.remove(&self.name);
        }
    }
}

struct KubernetesNode {
    namespace: Weak<KubernetesNamespace>,
    name: String,
}

impl KubernetesNode {
    pub fn new(namespace: &Arc<KubernetesNamespace>, name: &str) -> Arc<Self> {
        Arc::new(KubernetesNode {
            namespace: Arc::downgrade(namespace),
            name: name.to_string(),
        })
    }
}

What I'm trying to achieve is for each "parent" struct to hold "children" structs and still able to access field in the parent struct and call methods on it.

For example, let's take a scenario where I hold a Node, I want to be able to call mynode.destroy() and it will remove the entry from the parent Namespace

I'm trying to wrap my head around this but it seems like hardly feasible with my knowledge, because for example, when I call provider.create_node, the method will be called on the trait Provider and the trait is not implemented for Arc<KubernetesProvider> but for KubernetesProvider so I can pass around ````ArcwhereProvideris implemented forKubernetesProvider```.

Do you think there may be some way to achieve my intended result ? I'm open to suggestions, thank you for taking the time to read this !

If you want an example program (not compiling due to the above reasons) here is a gist with project setup + main.rs: main.rs · GitHub

l0r1s

One option is to make the Arc part of the method signature

use std::{
    collections::HashMap,
    sync::{Arc, Weak},
};

use async_trait::async_trait;
use tokio::sync::RwLock;
use uuid::Uuid;

#[async_trait]
trait Provider {
    async fn create_namespace(self: Arc<Self>) -> Arc<dyn Namespace>;
}

#[async_trait]
trait Namespace {
    async fn create_node(self: Arc<Self>) -> Arc<dyn Node>;
    async fn destroy(self: Arc<Self>);
}

#[async_trait]
trait Node {
    async fn destroy(&self);
}

struct KubernetesProvider {
    namespaces: RwLock<HashMap<String, Arc<KubernetesNamespace>>>,
    provider_field: String,
}

impl KubernetesProvider {
    fn new() -> Arc<Self> {
        Arc::new(KubernetesProvider {
            namespaces: RwLock::new(HashMap::new()),
            provider_field: "provider_field".to_string(),
        })
    }
}

#[async_trait]
impl Provider for KubernetesProvider {
    async fn create_namespace(self: Arc<Self>) -> Arc<dyn Namespace> {
        let name = format!("namespace_{}", Uuid::new_v4());
        let namespace = KubernetesNamespace::new(self.clone(), &name);

        self.namespaces
            .write()
            .await
            .insert(name, namespace.clone());

        namespace
    }
}

struct KubernetesNamespace {
    provider: Weak<KubernetesProvider>,
    name: String,
    nodes: RwLock<HashMap<String, Arc<KubernetesNode>>>,
}

impl KubernetesNamespace {
    fn new(provider: Arc<KubernetesProvider>, name: &str) -> Arc<Self> {
        Arc::new(KubernetesNamespace {
            provider: Arc::downgrade(&provider),
            name: name.to_string(),
            nodes: RwLock::new(HashMap::new()),
        })
    }
}

#[async_trait]
impl Namespace for KubernetesNamespace {
    async fn create_node(self: Arc<Self>) -> Arc<dyn Node> {
        let name = format!("node_{}", Uuid::new_v4());
        let node = KubernetesNode::new(&self, &name);

        self.nodes.write().await.insert(name, node.clone());

        node
    }

    async fn destroy(self: Arc<Self>) {
        if let Some(provider) = self.provider.upgrade() {
            provider.namespaces.write().await.remove(&self.name);
        }
    }
}

struct KubernetesNode {
    namespace: Weak<KubernetesNamespace>,
    name: String,
}

impl KubernetesNode {
    pub fn new(namespace: &Arc<KubernetesNamespace>, name: &str) -> Arc<Self> {
        Arc::new(KubernetesNode {
            namespace: Arc::downgrade(namespace),
            name: name.to_string(),
        })
    }
}

#[async_trait]
impl Node for KubernetesNode {
    async fn destroy(&self) {
        if let Some(namespace) = self.namespace.upgrade() {
            namespace.nodes.write().await.remove(&self.name);
        }
    }
}

#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
    let provider = KubernetesProvider::new();

    let namespace1 = provider.clone().create_namespace().await;
    let namespace2 = provider.clone().create_namespace().await;

    let node1 = namespace1.clone().create_node().await;
    let node2 = namespace2.clone().create_node().await;
    let node3 = namespace1.clone().create_node().await;
    let node4 = namespace2.create_node().await;

    node1.destroy().await;

    namespace1.destroy().await;

    Ok(())
}

The downside here is that &Arc<Self> doesn't work, so you have to take the Arc by value, which potentially means a lot of extra refcount traffic that you don't really need.

In theory you could implement the traits for Arc<...> instead e.g. Arc<KubernetesNode> etc, but then you end up needing to wrap the node in another Arc to return a dyn Node.

1 Like

Thanks you for your response, yes, I came around this solution but didn't liked the idea of having a Arc<Arc<Struct>> where Arc<Struct> implemented one of the trait.

I've finally come to a working solution using the Arc::new_cyclic to have a Weak inside the struct itself to make it able to pass it down the tree, here is the final implementation, which works like a charm without modifying the traits/implem, only the new methods:

struct KubernetesProvider {
    weak: Weak<KubernetesProvider>,
    namespaces: RwLock<HashMap<String, Arc<KubernetesNamespace>>>,
    provider_field: String,
}

impl KubernetesProvider {
    fn new() -> Arc<Self> {
        Arc::new_cyclic(|weak| KubernetesProvider {
            weak: weak.clone(),
            namespaces: RwLock::new(HashMap::new()),
            provider_field: "provider_field".to_string(),
        })
    }
}

#[async_trait]
impl Provider for KubernetesProvider {
    async fn create_namespace(&self) -> Arc<dyn Namespace> {
        let name = format!("namespace_{}", Uuid::new_v4());
        let namespace = KubernetesNamespace::new(&self.weak, &name);

        self.namespaces
            .write()
            .await
            .insert(name, namespace.clone());

        namespace
    }
}

struct KubernetesNamespace {
    weak: Weak<KubernetesNamespace>,
    provider: Weak<KubernetesProvider>,
    name: String,
    nodes: RwLock<HashMap<String, Arc<KubernetesNode>>>,
}

impl KubernetesNamespace {
    fn new(provider: &Weak<KubernetesProvider>, name: &str) -> Arc<Self> {
        Arc::new_cyclic(|weak| KubernetesNamespace {
            weak: weak.clone(),
            provider: provider.clone(),
            name: name.to_string(),
            nodes: RwLock::new(HashMap::new()),
        })
    }
}

#[async_trait]
impl Namespace for KubernetesNamespace {
    async fn create_node(&self) -> Arc<dyn Node> {
        let name = format!("node_{}", Uuid::new_v4());
        let node = KubernetesNode::new(&self.weak, &name);

        self.nodes.write().await.insert(name, node.clone());

        node
    }

    async fn destroy(&self) {
        if let Some(provider) = self.provider.upgrade() {
            provider.namespaces.write().await.remove(&self.name);
        }
    }
}

struct KubernetesNode {
    namespace: Weak<KubernetesNamespace>,
    name: String,
}

impl KubernetesNode {
    pub fn new(namespace: &Weak<KubernetesNamespace>, name: &str) -> Arc<Self> {
        Arc::new(KubernetesNode {
            namespace: namespace.clone(),
            name: name.to_string(),
        })
    }
}

#[async_trait]
impl Node for KubernetesNode {
    async fn destroy(&self) {
        if let Some(namespace) = self.namespace.upgrade() {
            namespace.nodes.write().await.remove(&self.name);
        }
    }
}