Skip to main content

6: Inter-cube calls

Beginner
Rust
Tutorial

Overview

Inter-cube calls can be used to update information between two or more cubes.

To demonstrate these inter-cube calls, you'll use an example project called "PubSub".

A common problem in both distributed and decentralized systems is keeping separate services (or cubes) synchronized with one another. While there are many potential solutions to this problem, a popular one is the publisher/subscriber pattern or "PubSub". PubSub is an especially valuable pattern on the BigFile as its primary drawback, message delivery failures, does not apply.

Prerequisites

Before getting started, assure you have set up your developer environment according to the instructions in the developer environment guide.

Then, download the sample project's files with the commands:

git clone https://github.com/thebigfilecom/examples/
cd examples/rust/pub-sub/

Viewing the cube code

This project is comprised of two cubes: publisher and subscriber.

The subscriber cube contains a record of topics. The publisher cube uses inter-cube calls to add topics to the record within the subscriber cube.

Let's take a look at the src/lib.rs file for each of these cubes.

src/publisher/src/lib.rs:

use candid::{CandidType, Principal};
use ic_cdk::update;
use serde::Deserialize;
use std::cell::RefCell;
use std::collections::BTreeMap;

type SubscriberStore = BTreeMap<Principal, Subscriber>;

thread_local! {
static SUBSCRIBERS: RefCell<SubscriberStore> = RefCell::default();
}

#[derive(Clone, Debug, CandidType, Deserialize)]
struct Counter {
topic: String,
value: u64,
}

#[derive(Clone, Debug, CandidType, Deserialize)]
struct Subscriber {
topic: String,
}

#[update]
fn subscribe(subscriber: Subscriber) {
let subscriber_principal_id = ic_cdk::caller();
SUBSCRIBERS.with(|subscribers| {
subscribers
.borrow_mut()
.insert(subscriber_principal_id, subscriber)
});
}

#[update]
async fn publish(counter: Counter) {
SUBSCRIBERS.with(|subscribers| {
// This example is explicitly ignoring the error.
for (k, v) in subscribers.borrow().iter() {
if v.topic == counter.topic {
let _call_result: Result<(), _> =
ic_cdk::notify(*k, "update_count", (&counter,));
}
}
});
}

In this code, you can see two inter-cube update calls: fn subscribe(subscriber: Subscriber) and async fn publish(counter: Counter). The first method allows for the publisher cube to make a call to the subscriber cube and subscribe to topics. The second method allows the publisher cube to publish information into a topic in the subscribers cube.

src/subscriber/src/lib.rs:

use candid::{CandidType, Principal};
use ic_cdk::{update, query};
use serde::Deserialize;
use std::cell::Cell;

thread_local! {
static COUNTER: Cell<u64> = Cell::new(0);
}

#[derive(Clone, Debug, CandidType, Deserialize)]
struct Counter {
topic: String,
value: u64,
}

#[derive(Clone, Debug, CandidType, Deserialize)]
struct Subscriber {
topic: String,
}

#[update]
async fn setup_subscribe(publisher_id: Principal, topic: String) {
let subscriber = Subscriber { topic };
let _call_result: Result<(), _> =
ic_cdk::call(publisher_id, "subscribe", (subscriber,)).await;
}

#[update]
fn update_count(counter: Counter) {
COUNTER.with(|c| {
c.set(c.get() + counter.value);
});
}

#[query]
fn get_count() -> u64 {
COUNTER.with(|c| {
c.get()
})
}

In this code, there are three main methods: two inter-cube update methods and a query method.

The first method, async fn setup_subscribe(publisher_id: Principal, topic: String) provides functionality for the publisher cube to subscribe to topics within the subscriber cube. This function is called by the publisher cube.

The second method, fn update_count(counter: Counter) updates the counter record for each published value in a topic within the subscriber cube.

The third method, fn get_count() -> u64 allows the Counter value to be queried and returned in a call.

Deploying the cubes

Now that you've taken a look at your cubes, let's deploy them.

Open a terminal window on your local computer, if you don’t already have one open.

Then run the commands:

dfx start --clean --background
dfx deploy

Making inter-cube calls

First, let's subscribe to a topic. For example, to subscribe to the "Apples" topic, use the command:

dfx cube call subscriber init '("Apples")'

Then, to publish a record to the "Apples" topic, use the command:

dfx cube call publisher publish '(record { "topic" = "Apples"; "value" = 2 })'

Then, you can query and receive the subscription record value with the command:

dfx cube call subscriber getCount

The output should resemble the following:

(2 : nat)

Next steps

Next, let's cover how to upgrade cubes.