Regarding Concurrency Testing using PocketIC

Testing Concurrent Pool Creation with PocketIC

Hey everyone,

I’ve been experimenting with concurrency testing using PocketIC and came across this documentation.

To test concurrency in my backend, I wrote a function that simulates multiple pool creation requests along with the necessary ICRC-2 approvals happening concurrently. Below is my test function:

#[test]
fn test_create_multiple_pools_concurrently_with_approval() {
    let (pic, backend_canister, ckbtc_canister, cketh_canister, _ckusdc_canister) = setup();

    // Principal for testing
    let hardcoded_principal = Principal::from_text("xkd3g-llatk-lmuv7-eoudm-qtjnr-iapqh-taggr-pwpmo-3rojt-pxkwo-4qe").unwrap();

    let mut pool_creation_msg_ids = Vec::new();
    let mut approval_msg_ids = Vec::new();

    let token_canisters = [ckbtc_canister, cketh_canister];

    // Submit multiple concurrent approval and pool creation calls
    for i in 0..4 {
        let token_canister = token_canisters[i % 2]; // Alternate between ckbtc and cketh

        // Approve operation for each pool creation
        let approval_args = ApproveArgs {
            fee: None,
            memo: None,
            from_subaccount: None,
            created_at_time: None,
            amount: Nat::from(10000000u64),
            expected_allowance: None,
            expires_at: None,
            spender: Account {
                owner: backend_canister,
                subaccount: None,
            },
        };

        let approval_encoded_args = candid::encode_args((approval_args,)).unwrap();
        let approval_msg_id = pic.submit_call(
            token_canister,
            hardcoded_principal,
            "icrc2_approve",
            approval_encoded_args,
        ).unwrap();

        approval_msg_ids.push(approval_msg_id);

        // Create pool with varying parameters
        let pool_data = Pool_Data {
            pool_data: vec![
                CreatePoolParams {
                    token_name: format!("Token{}", i),
                    balance: Nat::from(100000u64 * (i as u64 + 1)),
                    weight: Nat::from(10u64),
                    value: Nat::from(100u64 * (i as u64 + 1)),
                    ledger_canister_id: token_canister,
                    image: format!("image{}.png", i),
                }
            ],
            swap_fee: Nat::from(5u64),
        };

        let pool_encoded_args = candid::encode_args((pool_data,)).unwrap();
        let pool_msg_id = pic.submit_call(
            backend_canister,
            hardcoded_principal,
            "create_pools",
            pool_encoded_args,
        ).unwrap();

        pool_creation_msg_ids.push(pool_msg_id);
    }

    // Await all submitted approval calls and check results
    for msg_id in approval_msg_ids {
        let res = pic.await_call(msg_id).unwrap();
        assert!(matches!(res, WasmResult::Reply(_)), "Approval should be successful to proceed with pool creation");
    }

    // Await all submitted pool creation calls and check results
    for msg_id in pool_creation_msg_ids {
        let res = pic.await_call(msg_id).unwrap();
        match res {
            WasmResult::Reply(data) => {
                let result: Result<(), CustomError> = candid::decode_one(&data).unwrap();
                assert!(result.is_ok(), "Expected successful pool creation");
            },
            WasmResult::Reject(message) => {
                println!("Pool creation failed with message: {}", message);
                assert!(false, "Pool creation should not fail");
            },
        }
    }
}

What I’m Trying to Achieve

  • Testing concurrent update calls using PocketIC.
  • Submitting approval transactions (ICRC-2) before calling the create_pools function.
  • Ensuring multiple pool creations execute correctly without race conditions.

Question

  1. Does this test structure make sense for concurrency testing in PocketIC?

Would appreciate any feedback or suggestions from the community! :rocket:

This objective is achieved.

Although you submitted the approvals transactions before the create requests, the execution of all those calls can proceed in any order. In other words, there’s no guarantee that approvals are executed before creating pools. For the latter, you’d need to use the pattern:

let msg1 = pic.submit_call(approval1);
let msg2 = pic.submit_call(approval2);
pic.await_call(msg1);
pic.await_call(msg2);
let msg3 = pic.submit_call(creation1);
let msg4 = pic.submit_call(creation2);
pic.await_call(msg3);
pic.await_call(msg4);

This objective should be achieved, too, but I’d recommend confirming that by producing debug prints in your canisters and asserting the expected order of logs (e.g., start1, start2, end1, end2 as opposed to start1, end1, start2, end2).

2 Likes